面向协议不是银弹
先明白几个测试的概念
double 可以理解为置换,它是所有模拟测试对象的统称,我们也可以称它为替身。一般来说,当你创建任意一种测试置换对象时,它将被用来替代某个指定类的对象。
stub 可以理解为测试桩,它能实现当特定的方法被调用时,返回一个指定的模拟值。如果你的测试用例需要一个伴生对象来提供一些数据,可以使用 stub 来取代数据源,在测试设置时可以指定返回每次一致的模拟数据。
spy 可以理解为侦查,它负责汇报情况,持续追踪什么方法被调用了,以及调用过程中传递了哪些参数。你能用它来实现测试断言,比如一个特定的方法是否被调用或者是否使用正确的参数调用。当你需要测试两个对象间的某些协议或者关系时会非常有用。
mock 与 spy 类似,但在使用上有些许不同。spy 追踪所有的方法调用,并在事后让你写断言,而 mock 通常需要你事先设定期望。你告诉它你期望发生什么,然后执行测试代码并验证最后的结果与事先定义的期望是否一致。
fake 是一个具备完整功能实现和行为的对象,行为上来说它和这个类型的真实对象上一样,但不同于它所模拟的类,它使测试变得更加容易。一个典型的例子是使用内存中的数据库来生成一个数据持久化对象,而不是去访问一个真正的生产环境的数据库。
实践中,这些术语常常用起来不同于它们的定义,甚至可以互换。稍后我们在这篇文章中会看到一些库,它们自认为自己是 "mock 对象框架",但是其实它们也提供 stub 的功能,而且验证行为的方式也类似于我描述的 "spy" 而不是 "mock"。所以不要太过于陷入这些词汇的细节;我下这些定义更多的是因为要在高层次上区分这些概念,并且它对考虑不同类型测试对象的行为会有帮助。
如果你对不同类型的模拟测试对象更多的细节讨论感兴趣,Martin Fowler 的文章 "Mocks Aren't Stubs" 被认为是关于这个问题的权威讨论。
开始
class Webservice {
func loadUser() -> User? {
let json = self.load(URL(string: "/users/current")!)
return User(json: json)
}
func loadEpisode() -> Episode? {
let json = self.load(URL(string: "/episodes/latest")!)
return Episode(json: json)
}
private func load(_ url: URL) -> [AnyHashable:Any] {
URLSession.shared.dataTask(with: url)
// etc.
return [:] // should come from the server
}
}
- load -- afnetworking 或者你封装的通用网络调用接口
- loaduser -- networkapimanager
- user -- model
- episode -- model
- Webservice -- datasource or datacenter
目前为止还不错。但是如果你要测试apimanager
-
你要用一个东西替换网络请求。
-
or pass in a mock URLSession using dependency injection.
(这句话没理解 ??? 给URLSession 写断言?)
总结起来就是 为了验证 a 的功能, a调用了b 。
- 那么通过替换b的实现 ,交付给a一个仿真的数据。我们能够得到进一步测试a的正确性。
- 或者是直接在b里面写断言,判断b的输入结果和输出结果是否符合预期。
We could also define a protocol that URLSession conforms to and then pass in a test instance.
其实就是临时给b添加了一个检验方法而已。还是和mock方法。这个还算优雅吧。
稍微好测试点的改动
struct Resource<A> {
let url: URL
let parse: ([AnyHashable:Any]) -> A
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!, parse: User.init)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!, parse: Episode.init)
private func load<A>(resource: Resource<A>) -> A {
URLSession.shared.dataTask(with: resource.url)
// load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
let json: [AnyHashable:Any] = [:] // should come from the server
return resource.parse(json)
}
}
这是对资源的封装 ,通过泛型,定义了一组输入和输出。
- 输入资源:url
- 输出资源:对象A
现在我们测试对象user 和 episode 就不用想着mock 什么东西啦。
虽然感觉是这样子的,但是本质还是没理解: 因为输入输出都有了,就是一个结构而已,没什么好测的。 那么问题来了, 什么东西要测,什么东西不测呢?结构本省没有什么过程调用。
load 函数我们还是要测的
protocol FromJSON {
init(json: [AnyHashable:Any])
}
struct Resource<A: FromJSON> {
let url: URL
}
class Webservice {
let user = Resource<User>(url: URL(string: "/users/current")!)
let episode = Resource<Episode>(url: URL(string: "/episodes/latest")!)
private func load<A>(resource: Resource<A>) -> A {
URLSession.shared.dataTask(with: resource.url)
// load asynchronously, parse the JSON, etc. For the sake of the example, we directly return an empty result.
let json: [AnyHashable:Any] = [:] // should come from the server
return A(json: json)
}
}
上面看上去简单,但是缺少灵活性(上面是一种高内聚的改动)
上面的代码,怎么定义一个resource 包含 User数组呢。
The protocol makes things simpler, but I think it doesn’t pay for itself, because it dramatically decreases the ways in which we can create a Resource
struct Resource<A> {
let url: URL
let parse: ([AnyHashable:Any]) -> A
}
protocol FromJSON {
init(json: [AnyHashable:Any])
}
struct Resource<A: FromJSON> {
let url: URL
}
灵活性丧失在哪里?