面向协议不是银弹

面向协议不是银弹

先明白几个测试的概念

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 {
    let url: URL
    let parse: ([AnyHashable:Any]) -> A
}

class Webservice {
    let user = Resource(url: URL(string: "/users/current")!, parse: User.init)
    let episode = Resource(url: URL(string: "/episodes/latest")!, parse: Episode.init)
    
    private func load(resource: Resource) -> 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 {
    let url: URL
}

class Webservice {
    let user = Resource(url: URL(string: "/users/current")!)
    let episode = Resource(url: URL(string: "/episodes/latest")!)
    
    private func load(resource: Resource) -> 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 {
    let url: URL
    let parse: ([AnyHashable:Any]) -> A
}
protocol FromJSON {
    init(json: [AnyHashable:Any])
}

struct Resource {
    let url: URL
}

灵活性丧失在哪里?

你可能感兴趣的:(面向协议不是银弹)