如何创建在触发功能时通过的测试?
How to create a test that passes when a function is triggered?
我正在尝试对视图控制器中的两个函数进行单元测试 class。这两个函数将分别创建用户和登录用户。我的测试与 UI 无关。
截至目前,我只需要创建一个在调用其中一个函数时通过的测试。目标是尽可能在不更改当前视图控制器实现的情况下执行此操作,并将其全部保留在测试中 class/function。
我该怎么做?
谢谢
我假设视图控制器的方法调用 "create user" 或 "sign in user"。 (你说这与 UI 无关,但如果那是触发器,我们可以轻松测试按钮点击。)
class MyViewController: UIViewController {
func triggeringMethod() {
// Calls one of the two methods below
}
func createUser() {
// Does stuff
}
func signInUser() {
// Does stuff
}
}
而且听上去是想试流量,不想试效果。也就是说,你想测试 "Was createUser()
called?" 有几种方法可以得到你想要的东西,但更好的方法需要你改变你的视图控制器实现。
制作部分模拟
Michael Feathers 的 Working Effectively With Legacy Code 中的一个标准技巧是 "Subclass and Override Method"。让我们从那里开始。在测试代码中,我们可以使
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
}
}
现在可以调用触发方法,并查看调用次数。
(您可能必须做出的更改:class 不能是 final
。方法不能是 private
。)
移动工人
虽然这是一个很好的起点,但不要止步于此。我们创建的是 "partial mock"。这是我们保留大部分功能的地方,但模拟了几个方法。这是要避免的事情。原因是我们最终得到混合了生产代码和测试代码的 class。在您无意中测试测试代码而不是测试生产代码的情况下结束是太容易了。
部分模拟清楚地表明我们缺少边界。视图控制器做的太多了。 "create user" 和 "sign in user" 的实际工作应该由另一种类型执行(甚至可能是 2 种类型)。在Swift中,我们可以用一个协议来定义这个边界。这样生产代码可以使用真正的功能,而对于测试代码我们可以注入模拟。
这意味着生产代码应该避免自己决定谁来做实际工作。相反,我们应该 告诉 它谁在做这项工作。这样,测试可以提供替代工人。从外部指定这些依赖项称为 "Dependency Injection".
传回效应
另一个选项可以让我们完全避免模拟。我们可以在枚举中描述所需的效果,而不是测试是否调用了某些东西。然后我们可以定义像
这样的效果
enum Effect {
case createUser(CreateUserRequestModel)
case signInUser(SignInUserRequestModel)
}
触发方法调用 createUser()
或 signInUser()
,而不是调用委托。 (另一种选择是传递闭包而不是指定委托。)
protocol Delegate {
perform(_ effect: Effect)
}
然后在触发方法中,
delegate?.perform(.createUser(parameters))
这意味着将这些枚举值转换为实际工作取决于实际委托。但它使测试易于编写。我们所需要的只是提供一个捕获 Effect
值的测试实现。
我正在尝试对视图控制器中的两个函数进行单元测试 class。这两个函数将分别创建用户和登录用户。我的测试与 UI 无关。
截至目前,我只需要创建一个在调用其中一个函数时通过的测试。目标是尽可能在不更改当前视图控制器实现的情况下执行此操作,并将其全部保留在测试中 class/function。
我该怎么做?
谢谢
我假设视图控制器的方法调用 "create user" 或 "sign in user"。 (你说这与 UI 无关,但如果那是触发器,我们可以轻松测试按钮点击。)
class MyViewController: UIViewController {
func triggeringMethod() {
// Calls one of the two methods below
}
func createUser() {
// Does stuff
}
func signInUser() {
// Does stuff
}
}
而且听上去是想试流量,不想试效果。也就是说,你想测试 "Was createUser()
called?" 有几种方法可以得到你想要的东西,但更好的方法需要你改变你的视图控制器实现。
制作部分模拟
Michael Feathers 的 Working Effectively With Legacy Code 中的一个标准技巧是 "Subclass and Override Method"。让我们从那里开始。在测试代码中,我们可以使
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
}
}
现在可以调用触发方法,并查看调用次数。
(您可能必须做出的更改:class 不能是 final
。方法不能是 private
。)
移动工人
虽然这是一个很好的起点,但不要止步于此。我们创建的是 "partial mock"。这是我们保留大部分功能的地方,但模拟了几个方法。这是要避免的事情。原因是我们最终得到混合了生产代码和测试代码的 class。在您无意中测试测试代码而不是测试生产代码的情况下结束是太容易了。
部分模拟清楚地表明我们缺少边界。视图控制器做的太多了。 "create user" 和 "sign in user" 的实际工作应该由另一种类型执行(甚至可能是 2 种类型)。在Swift中,我们可以用一个协议来定义这个边界。这样生产代码可以使用真正的功能,而对于测试代码我们可以注入模拟。
这意味着生产代码应该避免自己决定谁来做实际工作。相反,我们应该 告诉 它谁在做这项工作。这样,测试可以提供替代工人。从外部指定这些依赖项称为 "Dependency Injection".
传回效应
另一个选项可以让我们完全避免模拟。我们可以在枚举中描述所需的效果,而不是测试是否调用了某些东西。然后我们可以定义像
这样的效果enum Effect {
case createUser(CreateUserRequestModel)
case signInUser(SignInUserRequestModel)
}
触发方法调用 createUser()
或 signInUser()
,而不是调用委托。 (另一种选择是传递闭包而不是指定委托。)
protocol Delegate {
perform(_ effect: Effect)
}
然后在触发方法中,
delegate?.perform(.createUser(parameters))
这意味着将这些枚举值转换为实际工作取决于实际委托。但它使测试易于编写。我们所需要的只是提供一个捕获 Effect
值的测试实现。