CodeGym /Java 博客 /随机的 /关于单元测试:技术、概念、实践
John Squirrels
第 41 级
San Francisco

关于单元测试:技术、概念、实践

已在 随机的 群组中发布
今天,您将找不到未进行测试的应用程序,因此这个主题对于新手开发人员来说将比以往任何时候都更相关:没有测试就无法成功。让我们考虑原则上使用什么类型的测试,然后我们将详细研究有关单元测试的所有知识。 关于单元测试:技术、概念、实践 - 1

测试类型

什么是测试?根据维基百科:“软件测试涉及软件组件或系统组件的执行,以评估一个或多个感兴趣的属性。” 换句话说,它是在某些情况下检查我们系统的正确性。好吧,让我们看看通常有哪些类型的测试:
  • 单元测试——旨在分别检查系统的每个模块的测试。这些测试应该应用于系统的最小原子部分,例如模块。
  • 系统测试——高级测试,用于检查应用程序的较大部分或整个系统的运行情况。
  • 回归测试 — 用于检查新功能或错误修复是否影响应用程序现有功能或引入旧错误的测试。
  • 功能测试——检查应用程序的一部分是否满足规范、用户故事等中规定的要求。

    功能测试的类型:

    • 白盒测试——在了解系统内部实现的情况下检查应用程序的一部分是否满足要求;
    • 黑盒测试——在不知道系统内部实现的情况下检查应用程序的一部分是否满足要求。

  • 性能测试 — 编写的测试以确定系统或系统的一部分在特定负载下的性能。
  • 负载测试 — 旨在检查系统在标准负载下的稳定性并找出应用程序仍能正常工作的最大负载的测试。
  • 压力测试 — 旨在检查应用程序在非标准负载下的性能并确定系统故障前的最大负载的测试。
  • 安全测试——用于检查系统安全性的测试(来自黑客、病毒、对机密数据的未授权访问以及其他令人愉快的攻击)。
  • 本地化测试——应用程序本地化测试。
  • 可用性测试——旨在检查可用性、可理解性、吸引力和可学习性的测试。
这一切听起来不错,但它在实践中是如何运作的呢?简单的!我们使用 Mike Cohn 的测试金字塔:关于单元测试:技术、概念、实践 - 2这是金字塔的简化版本:它现在被分成更小的部分。但今天我们不会太复杂。我们将考虑最简单的版本。
  1. 单元——这部分指的是单元测试,它们应用于应用程序的不同层。他们测试应用程序逻辑的最小可分单元。例如,类,但最常见的是方法。这些测试通常会尽可能地尝试将测试内容与任何外部逻辑隔离开来。也就是说,他们试图营造应用程序的其余部分正在按预期运行的错觉。

    应该总是有很多这样的测试(比任何其他类型都多),因为它们测试小块并且非常轻量级,不会消耗大量资源(意味着 RAM 和时间)。

  2. 集成——这部分是指集成测试。此测试检查系统的较大部分。也就是说,它要么组合多个逻辑片段(多个方法或类),要么检查与外部组件交互的正确性。这些测试通常比单元测试小,因为它们更重。

    集成测试的一个示例可以是连接到数据库并检查使用它的方法操作的正确性。

  3. UI——这部分指的是检查用户界面操作的测试。它们涉及应用程序所有级别的逻辑,这就是它们也被称为端到端测试的原因。通常,它们的数量要少得多,因为它们最麻烦并且必须检查最必要(使用)的路径。

    在上图中,我们看到三角形的不同部分大小不一:实际工作中不同种类的测试数量所占的比例大致相同。

    今天我们将仔细研究最常见的测试,单元测试,因为所有自尊的 Java 开发人员都应该能够在基本级别使用它们。

单元测试中的关键概念

测试覆盖率(代码覆盖率)是衡量应用程序测试情况的​​主要指标之一。这是测试覆盖的代码百分比 (0-100%)。实际上,许多人都追求这个百分比作为他们的目标。这是我不同意的事情,因为这意味着测试开始应用于不需要的地方。例如,假设我们的服务中有标准的 CRUD(创建/获取/更新/删除)操作,没有额外的逻辑。这些方法是纯粹的中介,将工作委托给与存储库一起工作的层。在这种情况下,我们没有什么可测试的,除了可能给定的方法是否调用 DAO 方法之外,但这是个玩笑。通常使用其他工具来评估测试覆盖率:JaCoCo、Cobertura、Clover、Emma 等。要更详细地研究这个主题, TDD代表测试驱动开发。在这种方法中,在执行任何其他操作之前,您编写一个测试来检查特定代码。结果证明这是黑盒测试:我们知道输入是什么,我们也知道输出应该是什么。这使得可以避免代码重复。测试驱动开发从为应用程序中的每个功能设计和开发测试开始。在 TDD 方法中,我们首先创建一个测试来定义和测试代码的行为。TDD 的主要目标是使您的代码更易于理解、更简单且无错误。关于单元测试:技术、概念、实践 - 3该方法包括以下内容:
  • 我们编写测试。
  • 我们运行测试。毫不奇怪,它失败了,因为我们还没有实现所需的逻辑。
  • 添加使测试通过的代码(我们再次运行测试)。
  • 我们重构代码。
TDD 基于单元测试,因为它们是测试自动化金字塔中最小的构建块。通过单元测试,我们可以测试任何类的业务逻辑。BDD代表行为驱动的开发。这种方法基于 TDD。更具体地说,它使用通俗易懂的语言示例为参与开发的每个人解释系统行为。我们不会深入研究这个术语,因为它主要影响测试人员和业务分析师。测试用例是描述检查被测代码所需的步骤、具体条件和参数的场景。测试夹具是将测试环境设置为具有成功运行被测方法所必需的状态的代码。它是一组预定义的对象及其在指定条件下的行为。

测试阶段

测试包括三个阶段:
  • 指定测试数据(夹具)。
  • 运行被测代码(调用被测方法)。
  • 验证结果并与预期结果进行比较。
关于单元测试:技术、概念、实践 - 4为确保测试模块化,您需要与应用程序的其他层隔离。这可以使用存根、模拟和间谍来完成。模拟是可以定制的对象(例如,为每个测试量身定制)。它们让我们指定我们对方法调用的期望,即预期的响应。我们使用模拟对象来验证我们是否得到了我们所期望的。存根在测试期间提供对调用的硬编码响应。它们还可以存储有关调用的信息(例如,参数或调用次数)。这些有时被称为间谍。有时人们会混淆存根和模拟这两个术语:不同之处在于存根不检查任何东西——它只模拟给定的状态。模拟是一个有期望的对象。例如,必须调用给定方法一定次数。换句话说,

测试环境

所以,现在进入正题。有几个可用于 Java 的测试环境(框架)。其中最流行的是 JUnit 和 TestNG。对于此处的审查,我们使用:关于单元测试:技术、概念、实践 - 5JUnit 测试是类中仅用于测试的方法。该类的名称通常与其测试的类相同,并在末尾附加“Test”。例如,CarService -> CarServiceTest。Maven 构建系统自动将这些类包含在测试范围内。其实这个类叫做测试类。让我们简要回顾一下基本注释:

  • @Test 表示该方法是一个测试(基本上,用这个注解标记的方法是一个单元测试)。
  • @Before 表示将在每次测试之前执行的方法。例如,用测试数据填充一个类,读取输入数据等。
  • @After 用于标记将在每次测试后调用的方法(例如清除数据或恢复默认值)。
  • @BeforeClass 放在方法之上,类似于@Before。但是这样的方法在给定类的所有测试之前只被调用一次,因此必须是静态的。它用于执行更多资源密集型操作,例如启动测试数据库。
  • @AfterClass 与@BeforeClass 相反:它对给定的类执行一次,但仅在所有测试之后执行。例如,它用于清除持久性资源或断开与数据库的连接。
  • @Ignore 表示一个方法被禁用并且在整个测试运行期间将被忽略。这用于各种情况,例如,如果基本方法已更改并且测试尚未返工以适应更改。在这种情况下,还需要添加描述,即@Ignore("Some description")。
  • @Test(expected = Exception.class) 用于否定测试。这些是验证方法在出现错误时的行为方式的测试,也就是说,测试预计该方法会抛出某种异常。这种方法由 @Test 注释指示,但指示要捕获哪个错误。
  • @Test(timeout = 100) 检查该方法是否在不超过 100 毫秒内执行。
  • @Mock 用于在字段上方分配模拟对象(这不是 JUnit 注释,而是来自 Mockito)。根据需要,我们直接在测试方法中针对特定情况设置模拟的行为。
  • @RunWith(MockitoJUnitRunner.class) 放在一个类之上。这个注释告诉 JUnit 调用类中的测试。有多种运行程序,包括:MockitoJUnitRunner、JUnitPlatform 和 SpringRunner。在 JUnit 5 中,@RunWith 注释已被更强大的@ExtendWith 注释所取代。
让我们看一下用于比较结果的一些方法:

  • assertEquals(Object expecteds, Object actuals) — 检查传递的对象是否相等。
  • assertTrue(boolean flag) — 检查传递的值是否为真。
  • assertFalse(boolean flag) — 检查传递的值是否为 false。
  • assertNull(Object object) — 检查传递的对象是否为空。
  • assertSame(Object firstObject, Object secondObject) — 检查传递的值是否指向同一个对象。
  • assertThat(T t, 匹配器 匹配器) — 检查 t 是否满足 matcher 中指定的条件。
AssertJ 还提供了一个有用的比较方法:assertThat(firstObject).isEqualTo(secondObject)。在这里我提到了基本方法——其他的是上述方法的变体。

实践测试

现在让我们以一个具体的例子来看一下上面的材料。我们将测试服务的更新方法。我们不会考虑 DAO 层,因为我们使用的是默认层。让我们为测试添加一个启动器:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <version>2.2.2.RELEASE</version>
   <scope>test</scope>
</dependency>
这里我们有服务类:

@Service
@RequiredArgsConstructor
public class RobotServiceImpl implements RobotService {
   private final RobotDAO robotDAO;

   @Override
   public Robot update(Long id, Robot robot) {
       Robot found = robotDAO.findById(id);
       return robotDAO.update(Robot.builder()
               .id(id)
               .name(robot.getName() != null ? robot.getName() : found.getName())
               .cpu(robot.getCpu() != null ? robot.getCpu() : found.getCpu())
               .producer(robot.getProducer() != null ? robot.getProducer() : found.getProducer())
               .build());
   }
}
第 8 行——从数据库中提取更新的对象。第 9-14 行——通过构建器创建一个对象。如果传入对象有字段,则设置它。如果没有,我们将保留数据库中的内容。现在看看我们的测试:

@RunWith(MockitoJUnitRunner.class)
public class RobotServiceImplTest {
   @Mock
   private RobotDAO robotDAO;

   private RobotServiceImpl robotService;

   private static Robot testRobot;

   @BeforeClass
   public static void prepareTestData() {
       testRobot = Robot
               .builder()
               .id(123L)
               .name("testRobotMolly")
               .cpu("Intel Core i7-9700K")
               .producer("China")
               .build();
   }

   @Before
   public void init() {
       robotService = new RobotServiceImpl(robotDAO);
   }
第 1 行——我们的 Runner。第 4 行——我们通过替换模拟将服务与 DAO 层隔离。第 11 行——我们为类设置了一个测试实体(我们将用作豚鼠的实体)。第 22 行——我们设置服务对象,这就是我们要测试的对象。

@Test
public void updateTest() {
   when(robotDAO.findById(any(Long.class))).thenReturn(testRobot);
   when(robotDAO.update(any(Robot.class))).then(returnsFirstArg());
   Robot robotForUpdate = Robot
           .builder()
           .name("Vally")
           .cpu("AMD Ryzen 7 2700X")
           .build();

   Robot resultRobot = robotService.update(123L, robotForUpdate);

   assertNotNull(resultRobot);
   assertSame(resultRobot.getId(),testRobot.getId());
   assertThat(resultRobot.getName()).isEqualTo(robotForUpdate.getName());
   assertTrue(resultRobot.getCpu().equals(robotForUpdate.getCpu()));
   assertEquals(resultRobot.getProducer(),testRobot.getProducer());
}
在这里我们看到测试分为三个明确的部分: 第 3-9 行 — 指定固定装置。第 11 行——执行被测代码。第 13-17 行——检查结果。更详细地说:第 3-4 行——设置 DAO 模拟的行为。第 5 行——设置我们将在标准之上更新的实例。第 11 行——使用该方法并获取结果实例。第 13 行——检查它是否不为空。第 14 行——比较结果的 ID 和给定的方法参数。第 15 行——检查名称是否已更新。第 16 行——查看 CPU 结果。第 17 行——我们没有在实例中指定这个字段,所以它应该保持不变。我们在这里检查该条件。让我们运行它:关于单元测试:技术、概念、实践 - 6测试是绿色的!我们可以松一口气了:)综上所述,测试提高了代码的质量,使开发过程更加灵活可靠。想象一下重新设计涉及数百个类文件的软件需要付出多大的努力。当我们为所有这些类编写单元测试时,我们可以自信地进行重构。最重要的是,它可以帮助我们在开发过程中轻松发现错误。伙计们,这就是我今天所拥有的。给我一个赞,然后发表评论:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION