在《你在测试金字塔的哪一层?(上)》中,我们领略了自动化测试的重要性,而测试金字塔,作为测试类型的架构体系,承载着单元、服务与UI三大测试层次。那么,究竟什么是单元测试、服务测试和UI测试呢?本期文章将为您揭开测试金字塔不同层次的神秘面纱。
让我们深入了解单元测试的魅力。单元测试是对程序模块(即软件设计的最小单位)进行的正确性检验。它旨在提高代码质量和可维护性。关于“一个单元”的概念,其实并没有标准答案,它取决于编程范式和语言环境。在函数式语言中,一个函数可以被视为一个单元。而在面向对象的语言环境中,一个方法或一个类都有可能成为测试的最小单元。
单元测试的魅力在于其广泛适用性,我们可以为各类产品代码进行单元测试,无需考虑它们的功能或层次。无论是controller、repository、领域类还是文件读写类,都可以进行单元测试。一个优秀的单元测试应坚持一个实现类对应一个测试类的原则,全面覆盖公共接口,同时避免过度关注实现细节。
编写单元测试时,我们应关注输入与输出的关系,而不是过度关注方法内部的实现过程。私有方法应被视为实现细节,无需过度测试。为了获得高效的测试覆盖率,我们应专注于测试公共接口,避免测试微不足道的代码。
当遇到需要测试私有方法的情况时,我们应该反思是否是设计出现了问题。很多时候,可能是方法过于复杂,通过公共接口测试需要大量数据和环境。这时,可以尝试对类进行拆分,将私有方法移至新的类中,然后通过旧类调用新类的方法。这样,原本难以测试的私有方法就变成了公共方法,可以轻松地为其添加测试,同时这种重构也改善了代码结构,更符合单一职责原则。
良好的测试结构至关重要,它遵循一种简单的模式:准备测试数据,调用被测方法,然后断言返回的结果是否符合预期。这种结构可以用口诀来记忆:“Arrange、Act、Assert”,或者“given、when、then”。这种测试结构不仅适用于单元测试,还可以应用于更高层次的测试。它让测试保持一致性,易于阅读,而且写出来的测试往往更加简短、更具表达力。一、重构后的ExampleController类与单元测试
在明确了要测试的目标和单元测试的构造方式后,让我们审视一个生动且简洁的ExampleController类:
```java
@RestController
public class ExampleController {
private final PersonRepository personRepo;
@Autowired
public ExampleController(PersonRepository personRepo) {
this.personRepo = personRepo;
}
@GetMapping("/hello/{lastName}")
public String hello(final String lastName) {
Optional
return foundPerson.map(person ->
String.format("Hello %s %s!", person.getFirstName(), person.getLastName()))
.orElseGet(() -> String.format("Who is this '%s' you're talking about?", lastName));
}
}
```
对于hello方法的单元测试,我们可以这样写:
```java
public class ExampleControllerTest {
private ExampleController controller;
@Mock
private PersonRepository personRepo;
@BeforeEach
public void setUp() {
MockitoAnnotations.openMocks(this); // Assume this is used for proper mocks initialization.
controller = new ExampleController(personRepo);
}
@Test
public void testHelloWithKnownLastName() {
Person mockPerson = new Person("Peter", "Pan"); // Assuming Person class exists with these fields and constructor.
Mockito.when(personRepo.findByLastName("Pan")).thenReturn(Optional.of(mockPerson));
String greeting = controller.hello("Pan"); // Assuming the controller method call is correct and returns a string.
assertEquals("Hello Peter Pan!", greeting); // Assuming this assertion is correct and can be used in Java tests.
}
//... (Other tests for unknown last names, etc.)
}
```
二、集成测试:应用与外部环境的融合之旅在实际应用中,软件通常需要与外部组件如数据库、文件系统等集成。虽然我们在编写单元测试时通常会避免涉及这些外部依赖,但这些交互在实际应用中确实存在,因此需要进行充分的测试覆盖。这就是集成测试的价值所在——它确保应用程序与其所有外部依赖项完美融合。
集成测试是软件开发中不可或缺的一环,特别是在涉及外部依赖的服务边界测试中。狭义的集成测试主要关注服务与外界的交互行为,如数据库、文件系统和外部服务等。这类测试至关重要,因为它确保了应用与外部系统的协同工作。
想象一下进行数据库集成测试的场景。启动数据库连接,然后调用应用中的函数,该函数将数据写入数据库。接着,读取数据库,验证期望的数据是否已正确写入。同样,对于通过REST API与外部服务集成的测试,也需要启动应用,并调用相关函数,验证应用是否能正确解析返回结果。
集成测试也可以是白盒测试,验证应用与依赖之间的正确交互。在测试过程中,一些框架允许对应用的特定部分进行模拟(mocking),以验证交互的正确性。
在编写集成测试时,任何涉及数据序列化和反序列化的地方都需要特别注意。这包括但不限于调用REST API、读写数据库、调用外部服务的API以及读写队列等。这些场景在测试中都需要详尽覆盖,以确保数据读写操作的正常进行。
在进行集成测试时,我们应尽可能在本地运行外部依赖。例如,使用本地的MySQL数据库、ext4文件系统等进行测试。对于无法本地运行的外部服务,可以考虑构建模拟服务或专用实例进行测试,以避免在生产环境中产生过多的测试请求,从而避免可能的干扰或DoS攻击。
在测试金字塔中,集成测试的层级高于单元测试。与单元测试相比,集成测试需要处理外部依赖,如数据库和文件系统等,因此可能需要更长的时间。虽然编写集成测试可能更具挑战性,但它们为验证应用与外部依赖的正确交互提供了信心。
以PersonRepository为例,这是一个依赖Spring Data的数据库类。我们无需实际实现它,只需继承CrudRepository接口并声明方法名。Spring会自动处理其余部分。其中findByLastName方法是一个自定义方法,用于根据姓氏获取人员对象。
尽管Spring Data已经为我们处理了大部分数据库交互,但进行数据库集成测试仍然很重要。这不仅能验证自定义方法的正确性,还能确保我们的数据库类正确利用Spring的装配特性并与数据库连接无误。
为了简化测试环境设置,我们选择在本地运行测试时使用内存数据库H2。在build.gradle中,我们已将H2定义为测试依赖项。在测试目录下的application.properties文件中不定义任何数据库连接属性,让Spring Data自动使用内存数据库进行测试。这种设置简化了环境配置并加速了测试过程。
三、UI测试探微除了我们常见的网页界面,大多数应用都拥有用户界面。但用户界面并不局限于我们所看到的五彩斑斓的网页,还包括REST API界面、命令行界面等多元形态。UI测试的核心目标是验证应用的用户界面能否按预期工作,包括但不限于用户的输入是否能触发正确的动作,数据是否能正确展示给用户,UI状态是否发生预期变化等。
有时,人们会将UI测试和端到端测试混为一谈。确实,端到端测试通常涵盖了大量的UI测试。UI测试并不一定要通过端到端的方式进行。依据技术栈的不同,UI测试可以简单到只针对前端的JavaScript代码进行单元测试,通过桩(stub)将后端隔离开来。
对于网页界面的测试,我们可以围绕行为、布局、可用性以及设计一致性等方面展开。测试应用的布局是否前后一致确实是一个挑战。由于应用类型和用户需求的多变性,我们需要确保代码的更改不会意外破坏页面的布局。值得注意的是,计算机在判断某物“看起来是否不错”方面一直表现不佳。
当我们想要测试可用性或者某些“看起来对不对”的要素时,这就超越了自动化测试的范围,涉及到探索性测试、可用性测试、走廊测试等领域。这时,我们需要展示产品给用户使用,观察他们是否喜欢,是否有任何功能让他们感到困惑。
针对已部署应用的用户界面测试,是典型的端到端测试(也被称为广域栈测试)。虽然端到端测试能让我们了解软件的整体运行情况,但它们通常较为脆弱,容易因各种问题而失败,错误信息也往往不是真正的根源。诸如浏览器差异、时间问题、元素渲染、意外弹出框等问题只是冰山一角,却需要耗费大量时间调试。
在微服务世界里,谁负责写这些测试是一个关键问题。端到端测试覆盖整个服务,使得编写端到端测试的权责变得模糊。虽然拥有一个集中式的QA团队来编写端到端测试看似是个好选择,但实际上这不符合DevOps的理念。真正的跨职能团队应该是担当此责的主体。关于谁应该负责端到端测试的问题,答案并非固定,取决于组织的具体情况。也许组织内的社区实践或质量协会等机构可以主导这方面的工作。
端到端测试需要大量的维护成本,运行速度较慢。除非微服务数量不多,否则在本地运行端到端测试几乎不可能,因为这需要启动所有服务。我们应尽量减少端到端测试的数量,聚焦于应用中对用户而言具有高价值的交互,定义产品核心价值的用户旅程,将最重要的步骤转化为自动化的端到端测试。例如,在电子商务网站中,用户搜索商品、添加到购物车并付款的用户旅程就是最有价值的部分,只需对此进行端到端测试即可。但切记避免过度测试带来的负担。
请记住,在测试金字塔中,低层级的测试已经全面覆盖了各种边缘情况和与其他系统的集成。高层级的测试不必重复这些测试内容。否则,过高的维护成本和大量的虚假错误报告将降低开发速度,最终使我们对测试失去信心。
文章来自《钓虾网小编|www.jnqjk.cn》整理于网络,文章内容不代表本站立场,转载请注明出处。