我假设视图控制器具有调用“创建用户”或“登录用户”的方法。 (你说它与 UI 无关,但如果这是触发器,我们可以轻松测试按钮点击。)
class MyViewController: UIViewController {
func triggeringMethod() {
// Calls one of the two methods below
}
func createUser() {
// Does stuff
}
func signInUser() {
// Does stuff
}
}
听起来你想测试流程,而不是效果。也就是说,您要测试“是否调用了createUser()?”有几种方法可以得到你想要的,但更好的方法需要你改变你的视图控制器实现。
制作部分模拟
Michael Feathers 的 Working Effectively With Legacy Code 中的一个标准技巧是“子类和覆盖方法”。让我们从那里开始。在测试代码中,我们可以制作
class TestableMyViewController: MyViewController {
override func createUser() {
}
override func signInUser() {
}
}
到目前为止,这是一种消除这些方法影响的方法。但是我们现在可以从我的尝试中添加模拟技术! Swift 东京谈话。
class TestableMyViewController: MyViewController {
var createUserCallCount = 0
var signInUserCallCount = 0
override func createUser() {
createUserCallCount += 1
}
override func signInUser() {
signInUserCallCount += 1
}
}
现在可以调用触发方法,查看调用次数了。
(您可能需要做的更改:类不能是final。方法不能是private。)
移动工人
虽然这是一个很好的起点,但不要止步于此。我们创建的是“部分模拟”。那是我们保留大部分功能的地方,但模拟了几个方法。这是要避免的。原因是我们最终得到了混合生产代码和测试代码的类。如果您无意中测试了测试代码而不是测试生产代码,这太容易结束了。
部分模拟清楚地表明我们缺少一个边界。视图控制器做的太多了。 “创建用户”和“登录用户”的实际工作应该由另一种类型(甚至可能是两种类型)执行。在 Swift 中,我们可以使用协议来定义这个边界。这样生产代码可以使用真正的功能,而对于测试代码我们可以注入模拟。
这意味着生产代码应避免自行决定由谁来执行实际工作。相反,我们应该告诉它是谁做的。这样,测试可以提供替代工人。从外部指定这些依赖项称为“依赖注入”。
回传效果
另一个选项让我们完全避免模拟。我们可以在枚举中描述期望的效果,而不是测试是否调用了某些东西。然后我们可以定义效果像
enum Effect {
case createUser(CreateUserRequestModel)
case signInUser(SignInUserRequestModel)
}
它不会调用createUser() 或signInUser() 的触发方法,而是调用委托。 (另一种选择是传入闭包而不是指定委托。)
protocol Delegate {
perform(_ effect: Effect)
}
然后在触发方法中,
delegate?.perform(.createUser(parameters))
这意味着由实际委托将这些枚举值转换为实际工作。但它使测试易于编写。我们所需要的只是提供一个捕获Effect 值的测试实现。