知道如何进行单元测试的绝佳过程(您的Swift代码)
我将向您展示一种方法,该方法可以确定应测试的内容以及一些概念,使编写测试时的生活更加轻松。
在我职业生涯的某个时刻,我知道单元测试的重要性,但是我不知道如何测试代码。 许多人都面临着同样的问题。 我们阅读了有关如何开始的文章,但仍然很难将我们学到的东西应用到我们自己的代码中。 通常,我们没有那些仅对两个参数求和并返回结果的函数,我们在许多示例中都可以看到它。 好吧,我遇到了一个过程,这对我来说真的很容易。
我要说的第一件事是,您无需更改体系结构即可开始单元测试。 当某些开发人员听到某种结构可以使代码更具可测试性时,这是一个错误。 然后他们等到转移到这种新的编码方式开始编写测试。 让我们不要那样做! 如果要测试现有代码,则可能需要重构某些部分,但绝对不要更改您的体系结构!
消息是对象在面向对象的世界中彼此交谈的方式。 您希望对象执行方法时将消息传递给该对象。 获取属性值是相同的,并且适用于其他所有条件!
知道要测试什么的过程包含三个简单的步骤 ,并且都涉及消息。
1.确定消息的类型
消息有两种可能的类型:查询和命令。 这可能很明显,但是查询是当您要求某事时,而命令是当您告诉它要某事时。 查询具有返回值,但是在命令没有返回值时却不会更改任何状态,但是会产生一些副作用。
查询 =返回什么,什么也没改变
命令 =不返回任何内容并更改某些内容
2.标识消息的来源
一条消息可能有三个来源:传入,传出和发送给自己。
传入 :当一个对象从外部(另一个对象)接收到消息时。
传出 :当对象向外部发送消息时。
自我发送 :猜猜是什么?! 当对象向自身发送消息时。
3.遵循以下图表:
哦! 多么棒的图表! 对?! 保持这种状态直到对您自然。
第一个例子是采用Equatable。
- 该函数具有返回值,并且不会更改任何状态。 这是一个查询!
- 它是公共的,可以由另一个想知道两个
Wallet
是否相等的对象调用。 进来! - 传入查询:声明结果。
传入查询非常简单。 您有一个预期的结果,然后调用该函数并断言它(如果它返回了预期的结果)。
(单词sut
代表sut
系统。在下面的示例中也将使用它)
第二个例子:在loadView
之后,您想确保您的插座正确钩住! 他们在这一点上不应该零。
- 该函数没有返回值,调用后应设置出口。 因此,它有副作用。 所以,这是一个命令! (副作用是直接公开的,因为出口是sut上的财产 )
- 该模块是公共的,可以从外部调用。 进来!
- 传入命令 :声明直接的公共副作用。
传入命令也应该很容易。 调用函数后,确保状态更改为期望值。
(文档说我们永远不要直接调用loadView
,所以我们使用loadViewIfNeeded
来触发它)
在转到下一个示例之前,我只想指出您可能具有一个既是查询又是命令的函数。 在这种情况下,您应该针对两种消息类型对其进行测试。
在下一个示例中,我们具有一个根据参数更改标签的函数。
- 该函数没有回报,并且有一些直接的公共副作用。 这是命令!
- 这是私人的! 因此,只能从ViewController本身调用它。 自发!
- 发送给自己的命令 :忽略!
为功能定义适当的访问控制很重要,这样可以更轻松地识别可能的来源。 我们不需要直接对其进行测试,因为当我们测试调用此私有函数的公共函数时,我们已经在对其进行间接测试。
到目前为止很容易,对吧? 让我们看看它是否保持这种状态!
现在,我们有了一个函数,该函数接收一个值并将其作为负值发送给另一个对象。
- 该函数没有返回值,但也没有任何直接的公共副作用! actually这里实际上的命令是存储对象上的
add(value:)
。 所以这是一个命令! - 如第一步所述,这里的原点是传出的,因为我们告诉另一个对象要做什么。
- 传出命令 :期望发送。
我们如何测试是否发送了命令? 您可以尝试以下方法:
但是,我们不仅在断言命令是否已发送! 我们正在测试,当我们调用addStorage
,store将值添加到其wallet属性。 这是对LocalStorage
本身的测试,而不是对我们的ViewModel的测试。 除了复制代码外,我们在测试中还完全耦合到了商店实现,这不是很好。 我们应该日晒测试我们的物体!
WalletViewModel
只需要了解add(value:)
函数,但了解更多。 为了解决这个问题,我们可以为LocalStore
创建一个接口,并使我们的对象依赖于此。
好多了! 现在将注入依赖项,并且我们的对象对实现一无所知! 在我们的测试中,我们可以注入仅在实际调用addValue
时对其进行addValue
的依赖项。
现在我们的测试和对象都没有耦合到LocalStorage
,我们仅检查是否addValue
(具有正确的值)。 我们在这里使用依赖注入 ! 我使用了最简单的方法,但没有使用最好的方法。 我建议您阅读更多有关它并了解其他方法。
我们已经介绍了我们需要测试的三个消息,而忽略了其余消息。 但是,我想提到最后一个示例,以明确依赖点。
- 直接查看
loadHistory
函数,因为viewDidLoad
loadHistory
执行任何操作。 这是它的声明:
func loadHistory(completion: @escaping ([EntryViewModel]) -> Void)
使用异步代码时,这有点棘手,但这也是一个查询! 该函数本身没有返回值,但是最终转义的闭包将被调用,并且其参数为结果。 - 它正在将消息发送到另一个对象。 外向!
- 传出查询 :忽略!
尽管我们不应该测试传出查询,但我们不应该依赖于依赖的实现! 对于我们的测试,如果它击中网络以加载历史记录该怎么办? 所有其他测试都需要等待,直到获取历史记录。 这不好! 我们的单元测试需要快速运行,并且到达网络的速度可能确实很慢。
我们需要再次使用依赖项注入,并在测试对象时注入TransactionViewModel
的测试版本。
我们已经使TransactionHistoryViewController
依赖于一个接口,为其创建了一个测试实现,并将其注入到我们的测试中。 我们不会测试传出查询,但是由于它是依赖项,因此我们将使用测试版本来处理可能的返回值。
我已经向您展示了用于测试的过程。 它改变了我的测试方式,并使我的代码更具可测试性。
我尝试足够通用,因此该知识也可以应用于其他编程语言。 而且我故意没有深入研究某些主题,例如Mocks / Stubs和Dependency Injection,因为我认为这太多了。
我很高兴我终于完成了这篇(长篇)帖子! 我仍然很难写作,而且要花好长时间才能取得好成绩。 希望对您有所帮助,如果您有任何疑问或反馈,请留在下面的评论中!
所示示例的完整代码
我第一次听说桑迪的方法的地方
Sandi Metz的测试魔术
苹果的可测性工程