API 测试的三个级别






4.92/5 (4投票s)
如何为您的 API 创建一个全面的测试套件
引言
绝大多数开发人员每天都在与 API 打交道。我们要么将第三方 API 集成到我们的应用程序中,要么开发 API 并使其可供其他开发人员使用。随着 API 开发的重要性日益增长,我们也意识到能够正确地测试这些 API 是一项至关重要的任务。API 的测试有各种方法和级别,在这里我想讨论三个测试级别,它们结合在一起可以提供全面的覆盖,并且在我最近的经验中效果非常好。
1. 单元测试
单元测试已经存在很长时间了,如果执行得当,已被证明非常有帮助。这个级别的测试适用于验证我们应用程序的单元,在面向对象语言中对应于一个类。非常重要的是要注意,单元测试应该只测试一个单元,并隔离其所有依赖项。软件模块相互依赖,但由于单元测试的目的是测试单元而不是其依赖项,因此我们通常会模拟依赖项并赋予它们预定义的行为,以验证被测试单元是否按我们期望的方式工作。
Mocking 是单元测试中的一个关键概念。在 Java 中,有许多 Mocking 框架可以与 JUnit 一起使用。例如,可以使用 JMockit、Mockito 和 EasyMock。我个人更喜欢 JMockit,但其他框架也应该提供相同的功能。这些框架通常利用注解来标记依赖对象,并提供工具来在被测试单元中调用它们时赋予它们预定义的行为。以下是 JMockit 的一个示例:
@Test
public void doBusinessOperationXyz(@Mocked final Dependency mockInstance)
{
...
new NonStrictExpectations() {{
...
// An expectation for an instance method:
mockInstance.someMethod(1, "test");
result = "mocked";
...
}};
// A call to the unit under test occurs here, leading to mock invocations
// that may or may not match specified expectations.
}
在这个单元测试中,我们使用 @Mocked 注解将 Dependency 对象标记为依赖项,并指定当 someMethod
分别使用参数 1 和 "test" 调用时,依赖对象应返回 "mocked" 字符串值。当被测试单元使用我们指定的参数调用 someMethod
时,JMockit 不会执行 Dependency 类的实际实现,而是接管控制并在运行时返回 "mocked" 字符串,忽略 someMethod
的实现。这样,我们就将该单元与其依赖项隔离,并专注于被测试单元的行为。
当我们必须处理数据库交互、外部服务或 I/O 操作时,Mocking 至关重要。当我们对依赖 DAO 模块来从任何数据源返回数据的类进行单元测试时,我们应该模拟这种交互,以防止实际调用数据源。原则上,单元测试不使用真正的数据库连接,不使用网络访问外部服务,也不执行 I/O 操作。编写单元测试时,我们需要记住,我们不是在测试被测试单元与其依赖项之间的交互。因此,最好使用模拟交互来保持测试的简洁、快速和可靠。
编写单元测试被认为是软件开发的一部分。我们应该尽可能经常运行它们,并将它们与应用程序的构建阶段关联起来。花时间保持单元测试覆盖率高是值得的,我们应该意识到我们的代码有多少百分比被单元测试覆盖。有像 Cobertura 这样的优秀工具可以跟踪应用程序中的单元测试覆盖率。它可以为你的源代码生成报告,并为你提供关于类行和分支覆盖率的宝贵信息,甚至可以显示哪些行没有被你的测试覆盖。更高级的工具如 Sonar,除了代码覆盖率,还可以分析你的代码并提供提高代码质量的建议,指出你可能存在 bug 的地方。
2. 集成测试
顾名思义,集成测试旨在测试软件不同组件之间的集成。与单元测试相反,集成测试不模拟依赖项。假设我们公开了一个 RESTful 端点,该端点以 JSON 格式返回从数据库获取的数据,我们可以编写一个集成测试来访问此端点并验证响应数据以及 HTTP 响应代码。在此测试中,Restful 服务将连接到真实数据库并执行适当的 SQL 查询。关键在于确保软件组件之间的交互是正确的。
集成测试为您的应用程序提供更广泛的覆盖,因为它们测试软件的多个组件。它们也更脆弱,因为它们可能受到环境变化的影响,并且它们必须依赖于系统其他部分的可访问性,例如数据源或外部 Web 服务。而在单元测试中,我们只依赖纯 Java 代码,这使得我们可以在每次构建时或每当我们更改一行代码并想知道是否破坏了任何东西时运行它们。此外,由于我们前面提到的原因,运行集成测试套件会花费更多时间。因此,将集成测试与单元测试分开,并且不像单元测试那样频繁地运行它们是很有意义的。一个好的做法可能是每次部署到测试环境后运行它们。
编写集成测试的一个经验法则是以一种可以一致地执行多次的方式来定义它们。当您一遍又一遍地运行测试,并在更改代码时看到自己破坏了什么时,软件测试的美就显现出来了。如果发现测试失败是因为它们的编写方式不正确,那将是令人沮丧的。我们的目标应该是编写可以多次运行的集成测试,这可以通过确保单个测试不会相互影响并且它们不依赖于前一个测试的结果来实现。
API 的集成测试可以通过简单地为服务端点编写客户端来完成。对于 RESTful API,任何 HTTP 客户端框架都可以正常工作。这里的一个巨大好处是客户端可以用任何语言编写,因为它基本上是发送 HTTP 请求并验证响应。像 Ruby 类的 Rspec 这样的框架正变得越来越流行,用于此类测试。此外,JAX-RS 实现,如 Jersey,提供了内置的测试框架,可以用于此目的。
单元测试和集成测试结合起来,可能会让你有信心认为你的 API 已准备好投入生产,但别被骗了!我们如何确保 API 在生产条件下仍然可以正常工作?
3. 性能测试
这个级别的测试是在我们知道所有功能都正常工作之后进行的,但我们也想了解 API 在高负载下的表现。性能测试被许多开发人员所忽视,但如果你的 API 将被多个消费者同时使用,进行性能测试以检测潜在的并发 bug 是非常重要的,这些 bug 在投入生产后很难重现和排查。这里的想法是从多个线程以随机顺序发出 API 调用,并在一段时间内监控应用程序的行为。
Apache JMeter 是完成此目的的完美工具。可以轻松地为服务端点创建性能测试脚本,并通过大量请求冲击测试机器。性能测试不仅有助于揭示应用程序中的并发问题,还能让你有机会监控应用程序如何使用机器上的 CPU 和内存资源。在性能测试期间监控这些资源将揭示内存泄漏、硬件资源不足或 Web 服务器、负载均衡器等的配置不当。可以说,性能测试是构建可扩展应用程序的关键。
监控服务器行为被认为是性能测试的一部分,为此使用监控工具始终是个好主意。虽然像 JMeter 这样的工具提供了服务器对每个请求响应的详细报告,但它并不知道性能测试期间服务器的状态。对于 Web 请求、数据库操作、服务器资源以及许多其他内容的全面监控,New Relic 是一个很棒的产品。
还值得一提的是持久性测试,这是性能测试的一种特殊形式。虽然性能测试通常会产生大量并行请求以创建高负载,但持久性测试会产生较少的请求,但运行时间更长。这里的重点是观察应用程序在几天甚至一周的正常使用负载下的行为。配置不当的环境可能导致无法预测的系统不一致,而你希望尽早发现这些问题。
结论
API 的这三个测试级别应该足以让你编写出可维护、功能齐全且可扩展的 API。起初,编写单元测试和集成测试似乎会增加开发时间,但随着时间的推移,你获得的收益将更加显著,并且它们将通过防止 bug 的发生来为你节省大量时间。此外,如果你有一个全面的测试套件,你一定会更加自信,并且不会因为害怕破坏某些东西而不敢重构你的代码。祝大家测试愉快。