我正在实现到Force.com REST的接口,对于我需要的API的哪些部分有非常具体的想法。我的问题是这个API抽象的使用者的可测试性。
我想出的第一件事是一个网关,每一个动作都有一个方法:
interface SalesforceApiGateway {
fun getLastUpdated(type: ObjectType, backTill: Duration = Duration.ofDays(1)): LastUpdated
fun getDetails(type: ObjectType, uuid: String, fields: List<String> = emptyList()): Details
data class LastUpdated(val error: Throwable? = null, val response: LastUpdatedResponse? = null)
data class Details(val error: Throwable? = null, val response: DetailsResponse? = null)
}使用者将得到这个接口@Injected的一个实例,因此测试可以很容易地实现该接口并将其传递给被测试的单元。
但是添加更多的对象类型和保持响应有意义和静态类型变得越来越困难。这就是为什么我转到了以下几个方面:
interface SalesforceApiClient: HttpClient, BackendAuthenticator<SalesforceCredentials> {}
sealed class SalesforceRequest<T>(val apiClient: SalesforceApiClient) {
fun getLastUpdated(backTill: Duration = Duration.ofDays(1)): List<String> {
val req: HttpGet = buildHttpRequest()
val resp = apiClient.execute(req)
return ObjectMapper().readValue(resp.entity.content,
object: TypeReference<List<String>>() {})
}
fun getDetails(uuid: String): T {
val req: HttpGet = buildDetailRequest(uuid)
val resp = apiClient.execute(req)
return ObjectMapper().readValue(resp.entity.content, getValueClass())
}
abstract fun getObjectType(): String
abstract fun getFields(): List<String>
abstract fun getValueClass(): Class<T>
class UserRequest(api: SalesforceApiClient): SalesforceRequest<User>(api) {
override fun getObjectType() = "USER"
override fun getFields() = User::class.members.map { it.name }
override fun getValueClass(): Class<User> = User::class.java
}
}瞧,键入的响应和一组请求,您可以在编译时进行推理。API消费者现在很高兴。
但我该怎么测试这个野兽呢?当然,我可以模拟SalesforceApiClient,它就是HttpClient和BackendAuthenticator的总和。但是,我觉得Force.com REST响应的结构知识被泄露到了面向UserRequest用户的单元测试中。这种感觉告诉我,我的课堂设计有问题。我怎样才能做得更好?
发布于 2016-06-08 15:05:51
我还没有找到一种方法来正确地测试/模拟SalesforceRequest在行使它的消费者。
它所做的就是让我意识到
Request,然后在其上提供多个“请求”-like方法是一个错误。每个SalesforceRequest子类型都是一个完整的API提供程序。SalesforceApiGateway类型安全(这是请求类的初始驱动程序)。最后,我得到了以下接口:
interface SalesforceObject {
val Id: String
val CreatedById: String
val LastModifiedDate: Instant
}
interface SalesforceApiGateway {
val API_VERSION: String
val MAX_RECENT_DAYS: Duration
fun <T: SalesforceObject> getLastUpdated(type: KClass<T>, backTill: Duration = Duration.ofDays(1)): LastUpdated
fun <T: SalesforceObject> getLastUpdated(type: KClass<T>, backTill: Instant): LastUpdated
= getLastUpdated(type, Duration.between(backTill, Instant.now(Clock.systemUTC())))
fun <T: SalesforceObject> getDetails(type: KClass<T>, uuid: String, fields: List<String> = type.members.map { it.name }): Details<T>
data class LastUpdatedResponse(val ids: List<String> = emptyList(),
val latestDateCovered: String = ISO_DATES.format(ZonedDateTime.now(Clock.systemUTC())))
data class LastUpdated(val error: Throwable? = null, val response: LastUpdatedResponse? = null)
data class DetailsResponse<T>(val wrapped: T)
data class Details<T>(val error: Throwable? = null, val response: DetailsResponse<T>? = null)
companion object {
val ISO_DATES = DateTimeFormatter.ISO_OFFSET_DATE_TIME
}
}好处是我现在可以使用Kotlin令人敬畏的默认参数来允许我的网关使用者不关心他们想要选择的字段,但是高级的将来的使用仍然可以覆盖这个选择。它还允许我表示,我希望网关实现负责对正确请求类型的正确反序列化响应。另外,我仍然可以保留我的界面,所以通过实现一个虚拟接口来进行测试是非常简单的。
在这个过程中有几点建议:
https://codereview.stackexchange.com/questions/131327
复制相似问题