“测试”一词最初是指“用于测定贵金属的小容器”。这意味着测试是一种确定黄金或白银质量的方法。它也用于精炼有价值的合金,如锡。
后来,该术语在其他领域被采用,如今,在教育,医学或软件开发等环境中经常会发现它。然而,它的本质并没有改变:测试被用来提炼最终价值。
我们在软件开发中使用测试来确保代码按预期工作。测试可以是手动的,也可以是自动的。手动测试类似于汽车制造商撞车,以验证它们在道路上是否安全。它可以工作,但经常这样做太昂贵了,所以它通常在生产周期结束时完成。这种方法的问题在于,在此阶段发现的问题可能会将产品的发布延迟数月。
自动化软件测试具有完全不同的成本结构。有一个初始反转和定期维护,但是一旦tes自动化到位,我们就可以根据需要随时运行测试 - 只需几分钱。
通过测试自动化,开发人员可以获得持续的反馈,使他们能够在生产周期的早期发现问题。快速迭代可改进设计、提高质量和更安全的发布。
测试自动化原理
整本书都是专门围绕测试自动化主题编写的。这是每个开发人员在某些时候都需要掌握的技能,最好尽早完成。
以下是简化学习曲线的六个原则:
- 测试应能提高质量。
- 测试应降低引入故障的风险。
- 测试有助于理解代码。
- 测试必须易于编写。
- 测试套件必须易于运行。
- 测试套件应该需要最少的维护。
原则 1:测试自动化提高质量
质量是一个难以捉摸的概念。尽我们所能,不可能用数字来定义它。然而,当我们看到它时,我们就知道它。软件行业提出了许多衡量质量的指标:缺陷数量、代码覆盖率、CI错误率、测试失败率等。每一个都抓住了质量概念的某些方面。
自动化测试通过持续运行数百或数千个测试来改善质量指标;在缺陷进入生产环境之前发现缺陷,通知开发人员潜在的问题,并检查系统是否偏离了用户的期望。
撇开指标不谈,我们知道可靠的设计是质量的先决条件。当测试驱动开发时,开发人员可以轻松尝试不同的想法,并确定哪一个效果最好。测试驱动开发 (TDD) 和行为驱动开发 (BDD) 等实践利用了这一特性,取得了巨大的成功。
原则2:测试自动化降低风险
代码审查和同行编程虽然必要且富有成效,但不能依靠它来查找错误。经验表明,更多的眼球并不能转化为更少的错误。
可靠地发现错误的唯一方法是构建一个全面的自动化测试套件。测试可以从上到下检查整个应用程序。它们在造成任何伤害、发现回归并在各种设备和环境上运行应用程序之前捕获错误,否则手动尝试的成本会非常高昂。
即使团队中的每个人都是一个非常聪明的开发人员,不知何故从未犯过错误,第三方依赖项仍然会引入错误并带来风险。自动测试可以扫描项目中的每一行代码,以查找错误和安全问题。
原则3:测试帮助您了解系统
很多时候,开发人员回到几天前编写的代码,却意识到他们已经完全忘记了它是如何工作的。当开发人员必须处理其他人编写的代码时,情况会更糟。
通常,阅读测试是理解系统的最佳场所,因为它们通过示例来展示事物的工作原理。因此,当有疑问时,开发人员可以参考测试套件。
例如,测试可以向其他开发人员展示API应该如何响应,从而允许他们跳过查看文档。
ctx := context.Background()result, _, err := env.Client.Server.Create(ctx, ServerCreateOpts { Name: "test", ServerType: &ServerType{ID: 1}, Image: &Image{ID: 2}, SSHKeys: []*SSHKey{ {ID: 1}, {ID: 2}, },})if err != nil { t.Fatalf("Server.Create failed: %s", err)}if result.Server == nil { t.Fatal("no server")}if result.Server.ID != 1 { t.Errorf("unexpected server ID: %v", result.Server.ID)}if result.RootPassword != "" { t.Errorf("expected no root password, got: %v", result.RootPassword)}if len(result.NextActions) != 1 || result.NextActions[0].ID != 2 { t.Errorf("unexpected next actions: %v", result.NextActions)}
不确定是否需要一行代码?注释掉它以查看哪个测试失败。有改进功能的想法吗?需要重构一段代码?尝试一下并运行自动测试。你会惊讶于你能从系统的测试中学到多少东西。
原则4:自动化测试应该易于编写
有些测试从手动测试开始,然后自动完成。但是,这通常会导致过于复杂,缓慢和尴尬的测试。当测试和代码具有一定的协同作用时,最好的结果就会出现。编写测试的行为促使开发人员生成更多的模块化代码,这反过来又使测试更简单,更精细。
测试的简单性很重要,因为为测试编写测试是不切实际的。代码也应该易于读取和写入。否则,我们就有可能在测试本身中引入失败,从而导致误报和片状。
许多测试框架使用域特定语言 (DSL) 以简单的英语定义测试。也许最值得注意的例子是小黄瓜测试框架使用的语言Gherkin:
Feature: Is it Friday yet?Everybody wants to know when it's FridayScenario: Sunday isn't Friday Given today is Sunday When I ask whether it's Friday yet Then I should be told "Nope"
总而言之,在编写测试时坚持一些基础知识是个好主意:
- 每个测试只写一个断言。
- 将代码与测试分开,即生产代码不应包含测试。
- 保持测试彼此独立,因为依赖关系很快就会滚雪球般地变成令人头痛的混乱。
- 将测试重叠保持在最低限度,即不要测试相同的代码两次。
- 不要破坏测试代码的封装。不,仅测试外部接口。
原则5:测试应易于运行
如果开发人员需要打开清单才能开始测试运行,则测试的运行频率不会达到应有的水平。
理想情况下,每次代码更改时都会运行测试,而无需任何干预。我们在这里很幸运,因为开发人员工具非常复杂。大多数现代IDE可以检测文件中的更改并自动启动测试套件,同样可以通过nodemon,live reload,fswatch或testmon等命令行程序来实现。
说明:VS 代码在后台运行测试
为了使测试易于运行,必须满足一些条件:
- 幂等性:测试不应有副作用。副作用包括写入文件、保存到数据库或通常更改数据。开发人员应该能够安全地运行任意次数的相同测试。
- 确定性:在给定相同输入的情况下,测试应始终给出相同的结果。当测试需要开发人员无法控制的外部数据(例如日期/时间或来自 API 的响应)时,应使用模拟或存根伪造这些数据。
- 独立:测试应该彼此独立,开发人员必须能够以任何顺序运行它们。
- 轻量级:测试必须足够轻量级,以便在合理的时间内在开发人员的计算机上运行。
- 粒度:开发人员必须能够逐个运行测试套件。
在开发人员的机器上运行测试只是等式的一部分。测试还必须在持续集成管道中进行。您的 CI/CD 管道充当质量门;它在每次提交时运行测试套件,提供即时反馈,并允许开发人员检测何时引入故障。
原则6:自动化测试套件需要低维护
最后一个原则是前五个原则的必然结果。也就是说,如果你很好地满足了其他人,你就可以免费获得它。尽管如此,这很重要,所以最好把它说出来。
开发人员希望做有创意和有益的工作。自动化使机器能够处理测试的苦差事。当测试易于编写且频繁执行时,将创建正反馈循环。开发人员倾向于欣赏自动化如何使他们的生活更轻松,因此被激励编写和维护测试。
当然,需要一些定期维护来保持测试的良好状态。以下是编写和维护测试套件的四条建议:
- 编写足够多的测试以使其有效(但不是更多)。如果错误溜走,则需要更多的测试。相反,如果您发现测试因小更改而中断,则需要删除一些测试。
- 选择适合该情况的最佳测试类型。单元测试快速且以激光为重点,而端到端测试涵盖 UI,并且更繁重且更全面。遵循测试金字塔的测试套件具有健康的各种测试。
- 保持测试的可靠性。当代码正确时失败的测试称为误报。有时无缘无故失败的测试称为片状测试。两者都会在测试套件中引起问题,因为它们是巨大的时间浪费者和挫折感的来源。
- 保持测试速度快。一个缓慢的测试套件将阻碍开发。
结论
那些认为测试成本高昂的人并不完全意识到质量差的代价。单独来看,错误和缺陷对产品价值的影响可能难以衡量,但如果它们得不到解决,它们可能会迅速失控。幸运的是,您可以通过构建和完善自动化测试套件来防止这种情况,从而为出色的开发人员体验和出色的高质量软件奠定基础。