65.9K
CodeProject 正在变化。 阅读更多。
Home

自动化测试的教条和探索

2018 年 11 月 12 日

CPOL

5分钟阅读

viewsIcon

4395

自动化测试的教条和探索

引言

之前的帖子中,我曾暗示过今天的话题。所以在这篇文章中,我想讨论两个话题。

  1. 自动化测试信条——这是基于我对我们开发者需要放宽掌握的看法的。
  2. 探索性测试——为什么以及在哪里应该在我们的测试中引入随机性和不可预测性。

由于这基本上是一篇“牢骚”式的文章,我们将讨论方法和技术,所以代码示例将尽量少。

自动化测试信条

我想稍微讨论一下被认为是“最佳”的测试方法。首先,我们将讨论每个测试的断言数量。

每个测试一个断言

你可能听说过著名的每个测试一个断言的方法。嗯,我个人认为这个方法应该慎用,并在适当的时候正确使用。例如,让我们看一下下面的代码:

void Main()
{
    User existingUser = new User();

    UserHasLoggedIn(existingUser);

    User updatedUser = GetUpdatedUser(existingUser);

    Assert.That(updatedUser.IsLoggedIn); // usually things end here

    // what I also do
    Assert.That(updatedUser.DateOfBirth, Is.EqualTo(existingUser.DateOfBirth));
    Assert.That(updatedUser.Name, Is.EqualTo(existingUser.Name));
}

// Simulates receiving the updated user from the database.
User GetUpdatedUser(User existingUser)
{
    return new User
    {
        DateOfBirth = existingUser.DateOfBirth,
        IsLoggedIn = existingUser.IsLoggedIn,
        Name = existingUser.Name
    };
}

public void UserHasLoggedIn(User user)
{
    user.IsLoggedIn = true;
    SaveProductToDatabase(user);
}

public void SaveProductToDatabase(User user)
{
    // ... do save logic
}

public class User 
{
    public string Name { get; set; }
    public bool IsLoggedIn { get; set; }
    public DateTime DateOfBirth { get; set; }
}

让我们考虑 `Main` 方法是我们的测试方法,并且我们有一个更新用户的流程,这样我们就知道他们已经登录(这是示例性的,不需要深入研究这个😀)。通常有些人只会断言所做的更改,仅此而已,但我觉得整个对象的状态也需要被断言,以确保没有任何东西以意外的方式改变它们,这就是关于测试的常见材料教给我们的,即每个测试只有一个断言。

所以在这个例子中,我展示了我对每个测试的断言数量的方法,断言越多,越安全,只是不要疯狂地断言对应用程序没有价值的东西,先思考再断言。🙂

单元测试与集成测试及混合

本节的第二个话题是单元测试和集成测试之间的混淆。正如你可能已经知道的,我是一个测试驱动开发(TDD)的忠实拥护者,我必须先写测试,然后实现,然后重构。好吧,当你采用这种方法时,你也会从中学习到一些东西,是的,单元测试很好,但是为每个类保留一个单一的测试类,特别是如果有一个条件涉及到增加测试数量,会变得很繁琐,你可能会发现自己多次测试同一件事情,而通过 TDD,你更关心的是这些组件在一起的正确行为,而不是仅仅增加你的代码覆盖率(这仍然会增加,因为代码Anyway需要走那条路径)。

有时候测试单个组件是完全没问题的,但有些组件不能孤立存在。例如,我更喜欢在测试使用 Entity Framework 的类时使用内存数据库的方法,而不是用某种集合来模拟它们。原因在于,如果没有持久化层,这个类本身并没有什么特别有趣的地方,这让我从“单元测试”阶段进入了“集成测试”阶段,但它是内存中的,是一个特殊的测试场景,运行速度可能甚至和它的“单元”对应物一样快。

现在,我不太喜欢给事物贴标签,称之为“集成测试”或“单元测试”并不能带来太多价值,我只关心应用程序和它所走的路线在需要的时候能够正常工作。

所以我的方法将是进行轻量级的集成测试,它们是单元测试的混合(因为它们测试一个场景),但带有模拟(用于缺失的组件,如 ASP.NET Core 的可注入项)和具体实现(例如 Entity Framework 的内存 SQLite 配置或 `HttpContext`),因为它们Anyway需要协同工作。

将单元测试细分为“只测试这个,代码不应触及任何其他东西”,而将集成测试定义为“设置一个测试数据库环境和服务器”,在我看来是不切实际的,并且陷入了“只顾着看树木而忽略了森林”的思维方式。

现在,我们来讨论下一个话题。

探索性测试

这与我关于进行探索性测试的上一篇帖子有关。

所以当我们编写测试时,我们通常会为已知行为分配一些值,而测试框架通过使用 `xUnit` 中的 `[Theory()]` 属性或 `nUnit` 中的 `[TestCase]` 属性来帮助我们。但在这方面存在一个缺陷,那就是我们提供了输入和预期的输出集。当然,只要组件行为正常,这就可以了。

我建议,除了测试我们组件核心场景的测试之外,我们还应该引入一些运行时间更长、带有随机性的测试,这些测试可以按需运行。这里有几个例子:

  • 使用上一篇文章中的 `RandomnStringGenerator`,我循环运行用户创建过程约 100 次,以检查即使我的规则设置为生成一个合适的密码,也没有隐藏的场景会导致注册失败。
  • 在进行数字计算时,生成一系列随机数,并检查是否没有任何错误,包括 0 和负数。
  • 在测试日期时,创建一个随机日期生成器,它还可以模拟来自不同时区的日期和时间,然后运行你的测试几十次,看看你实际上是在处理一个有效的东西,而不是仅仅是系统时钟。

这样,我们不仅测试了好的和坏的路径,也测试了混乱的、我们没想到的路径。

结论

我知道这是一篇又长又啰嗦的文章,感谢你的耐心阅读我的牢骚。我希望你从中吸取了一些教训,并且它将来会对你有帮助。

干杯,下次见。

© . All rights reserved.