知道如何进行单元测试的绝佳过程(您的Swift代码)

我将向您展示一种方法,该方法可以确定应测试的内容以及一些概念,使编写测试时的生活更加轻松。

在我职业生涯的某个时刻,我知道单元测试的重要性,但是我不知道如何测试代码。 许多人都面临着同样的问题。 我们阅读了有关如何开始的文章,但仍然很难将我们学到的东西应用到我们自己的代码中。 通常,我们没有那些仅对两个参数求和并返回结果的函数,我们在许多示例中都可以看到它。 好吧,我遇到了一个过程,这对我来说真的很容易。

我要说的第一件事是,您无需更改体系结构即可开始单元测试。 当某些开发人员听到某种结构可以使代码更具可测试性时,这是一个错误。 然后他们等到转移到这种新的编码方式开始编写测试。 让我们不要那样做! 如果要测试现有代码,则可能需要重构某些部分,但绝对不要更改您的体系结构!

消息是对象在面向对象的世界中彼此交谈的方式。 您希望对象执行方法时将消息传递给该对象。 获取属性值是相同的,并且适用于其他所有条件!

知道要测试什么的过程包含三个简单的步骤 ,并且都涉及消息。

1.确定消息的类型

消息有两种可能的类型:查询和命令。 这可能很明显,但是查询是当您要求某事时,而命令是当您告诉它要某事时。 查询具有返回值,但是在命令没有返回值时却不会更改任何状态,但是会产生一些副作用。

查询 =返回什么,什么也没改变
命令 =不返回任何内容并更改某些内容

2.标识消息的来源
一条消息可能有三个来源:传入,传出和发送给自己。

传入 :当一个对象从外部(另一个对象)接收到消息时。
传出 :当对象向外部发送消息时。
自我发送 :猜猜是什么?! 当对象向自身发送消息时。

3.遵循以下图表:

哦! 多么棒的图表! 对?! 保持这种状态直到对您自然。

第一个例子是采用Equatable。

  1. 该函数具有返回值,并且不会更改任何状态。 这是一个查询!
  2. 它是公共的,可以由另一个想知道两个Wallet是否相等的对象调用。 进来!
  3. 传入查询:声明结果。

传入查询非常简单。 您有一个预期的结果,然后调用该函数并断言它(如果它返回了预期的结果)。

(单词sut代表sut系统。在下面的示例中也将使用它)


第二个例子:在loadView之后,您想确保您的插座正确钩住! 他们在这一点上不应该零。

  1. 该函数没有返回值,调用后应设置出口。 因此,它有副作用。 所以,这是一个命令! (副作用是直接公开的,因为出口是sut上的财产
  2. 该模块是公共的,可以从外部调用。 进来!
  3. 传入命令 :声明直接的公共副作用。

传入命令也应该很容易。 调用函数后,确保状态更改为期望值。

(文档说我们永远不要直接调用loadView ,所以我们使用loadViewIfNeeded来触发它)

在转到下一个示例之前,我只想指出您可能具有一个既是查询又是命令的函数。 在这种情况下,您应该针对两种消息类型对其进行测试。


在下一个示例中,我们具有一个根据参数更改标签的函数。

  1. 该函数没有回报,并且有一些直接的公共副作用。 这是命令!
  2. 这是私人的! 因此,只能从ViewController本身调用它。 自发!
  3. 发送给自己的命令 :忽略!

为功能定义适当的访问控制很重要,这样可以更轻松地识别可能的来源。 我们不需要直接对其进行测试,因为当我们测试调用此私有函数的公共函数时,我们已经在对其进行间接测试。

到目前为止很容易,对吧? 让我们看看它是否保持这种状态!


现在,我们有了一个函数,该函数接收一个值并将其作为负值发送给另一个对象。

  1. 该函数没有返回值,但也没有任何直接的公共副作用! actually这里实际上的命令是存储对象上的add(value:) 。 所以这是一个命令!
  2. 如第一步所述,这里的原点是传出的,因为我们告诉另一个对象要做什么。
  3. 传出命令 :期望发送。

我们如何测试是否发送了命令? 您可以尝试以下方法:

但是,我们不仅在断言命令是否已发送! 我们正在测试,当我们调用addStorage ,store将值添加到其wallet属性。 这是对LocalStorage本身的测试,而不是对我们的ViewModel的测试。 除了复制代码外,我们在测试中还完全耦合到了商店实现,这不是很好。 我们应该日晒测试我们的物体!

WalletViewModel只需要了解add(value:)函数,但了解更多。 为了解决这个问题,我们可以为LocalStore创建一个接口,并使我们的对象依赖于此。

好多了! 现在将注入依赖项,并且我们的对象对实现一无所知! 在我们的测试中,我们可以注入仅在实际调用addValue时对其进行addValue的依赖项。

现在我们的测试和对象都没有耦合到LocalStorage ,我们仅检查是否addValue (具有正确的值)。 我们在这里使用依赖注入 ! 我使用了最简单的方法,但没有使用最好的方法。 我建议您阅读更多有关它并了解其他方法。


我们已经介绍了我们需要测试的三个消息,而忽略了其余消息。 但是,我想提到最后一个示例,以明确依赖点。

  1. 直接查看loadHistory函数,因为viewDidLoad loadHistory执行任何操作。 这是它的声明:
    func loadHistory(completion: @escaping ([EntryViewModel]) -> Void)
    使用异步代码时,这有点棘手,但这也是一个查询! 该函数本身没有返回值,但是最终转义的闭包将被调用,并且其参数为结果。
  2. 它正在将消息发送到另一个对象。 外向!
  3. 传出查询 :忽略!

尽管我们不应该测试传出查询,但我们不应该依赖于依赖的实现! 对于我们的测试,如果它击中网络以加载历史记录该怎么办? 所有其他测试都需要等待,直到获取历史记录。 这不好! 我们的单元测试需要快速运行,并且到达网络的速度可能确实很慢。

我们需要再次使用依赖项注入,并在测试对象时注入TransactionViewModel的测试版本。

我们已经使TransactionHistoryViewController依赖于一个接口,为其创建了一个测试实现,并将其注入到我们的测试中。 我们不会测试传出查询,但是由于它是依赖项,因此我们将使用测试版本来处理可能的返回值。

我已经向您展示了用于测试的过程。 它改变了我的测试方式,并使我的代码更具可测试性。

我尝试足够通用,因此该知识也可以应用于其他编程语言。 而且我故意没有深入研究某些主题,例如Mocks / Stubs和Dependency Injection,因为我认为这太多了。

我很高兴我终于完成了这篇(长篇)帖子! 我仍然很难写作,而且要花好长时间才能取得好成绩。 希望对您有所帮助,如果您有任何疑问或反馈,请留在下面的评论中!

所示示例的完整代码
我第一次听说桑迪的方法的地方
Sandi Metz的测试魔术
苹果的可测性工程