JavaRush /Java 博客 /Random-ZH /Java 单元测试:技术、概念、实践

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

已在 Random-ZH 群组中发布
今天,您几乎找不到没有测试的应用程序,因此这个主题对于新手开发人员来说比以往任何时候都更加相关:没有测试,您将一事无成。作为广告,我建议你看看我以前的文章。其中一些涵盖测试(即使如此,这些文章也将非常有用):
  1. 使用MariaDB替换MySql的数据库集成测试
  2. 多语言应用的实施
  3. 将文件保存到应用程序并将有关它们的数据保存到数据库
让我们考虑一下原则上使用哪些类型的测试,然后我们将详细研究您需要了解的有关单元测试的所有内容。

测试类型

什么是测试?正如维基百科所说:“测试测试是通过将系统置于不同的情况下并跟踪其中可观察到的变化来研究系统底层过程的一种方法。” 换句话说,这是对我们系统在某些情况下能否正确运行的测试。关于单元测试:方法、概念、实践 - 2好吧,让我们看看有哪些类型的测试:
  1. 单元测试是其任务是单独测试系统的每个模块的测试。期望这些是系统的最小可分割部分,例如模块。

  2. 系统测试是一种高级测试,用于测试应用程序的较大部分或整个系统的运行情况。

  3. 回归测试是用于检查新功能或错误修复是否影响应用程序现有功能以及旧错误是否再次出现的测试。

  4. 功能测试是检查应用程序的一部分是否符合规范、用户故事等中规定的要求。

    功能测试的类型:

    • “白盒”测试,在了解系统内部实现的情况下,测试部分应用程序是否符合要求;
    • “黑盒”测试用于在不了解系统内部实现的情况下部分应用程序是否符合要求。
  5. 性能测试是一种测试,旨在确定系统或其一部分在特定负载下运行的速度。
  6. 负载测试- 旨在检查系统在标准负载下的稳定性并找到应用程序正常工作的最大可能峰值的测试。
  7. 压力测试是一种测试,旨在检查非标准负载下应用程序的功能,并确定系统不会崩溃的最大可能峰值。
  8. 安全测试- 用于检查系统安全性的测试(免受黑客攻击、病毒、未经授权访问机密数据和其他生活乐趣)。
  9. 本地化测试是应用程序的本地化测试。
  10. 可用性测试是一种旨在检查用户的可用性、可理解性、吸引力和可学习性的测试。
  11. 这一切听起来不错,但在实践中它是如何运作的呢?很简单:使用迈克·科恩的测试金字塔: 关于单元测试:方法、概念、实践 - 4这是金字塔的简化版本:现在它被分为更小的部分。但今天我们不会曲解并考虑最简单的选择。
    1. 单元- 在应用程序的各个层中使用的单元测试,测试应用程序的最小可分逻辑:例如,一个类,但最常见的是一个方法。这些测试通常尝试尽可能地与外部逻辑隔离,即创建应用程序的其余部分正在标准模式下工作的假象。

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

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

      作为集成测试的示例,您可以考虑连接到数据库并检查与其一起使用的方法的正确执行

    3. UI - 检查用户界面操作的测试。它们影响应用程序各个级别的逻辑,这就是它们也被称为端到端的原因。通常,它们的数量要少得多,因为它们是最重量级的并且必须检查最必要的(使用的)路径。

      在上图中,我们看到三角形不同部分的面积之比:在实际工作中这些测试的数量大致保持相同的比例。

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

    单元测试的关键概念

    测试覆盖率(Code Coverage)是应用程序测试质量的主要评估之一。这是测试覆盖的代码百分比 (0-100%)。在实践中,许多人追逐这个百分比,我不同意这一点,因为他们开始在不需要的地方添加测试。例如,我们的服务具有标准的 CRUD(创建/获取/更新/删除)操作,无需额外的逻辑。这些方法纯粹是中介,将工作委托给与存储库一起工作的层。在这种情况下,我们没有什么可以测试的:也许这个方法是否调用了道中的方法,但这并不严重。为了评估测试覆盖率,通常会使用额外的工具:JaCoCo、Cobertura、Clover、Emma等。要更详细地研究这个问题,请保留几篇合适的文章: TDD(测试驱动开发) ——测试驱动开发。在这种方法中,首先编写一个测试来检查特定代码。事实证明这是黑盒测试:我们知道输入是什么,我们知道输出应该发生什么。这避免了代码重复。测试驱动开发从为应用程序的每个小功能设计和开发测试开始。在 TDD 方法中,首先开发一个测试来定义和验证代码将做什么。TDD 的主要目标是使代码更清晰、更简单且无错误。 关于单元测试:方法、概念、实践 - 6该方法由以下部分组成:
    1. 我们正在编写我们的测试。
    2. 我们运行测试,无论它是否通过(我们看到一切都是红色的 - 不要惊慌:这就是它应该的样子)。
    3. 我们添加应该满足此测试的代码(运行测试)。
    4. 我们重构代码。
    基于单元测试是测试自动化金字塔中最小的元素这一事实,TDD 就以它们为基础。借助单元测试,我们可以测试任何类的业务逻辑。 BDD(行为驱动开发) ——通过行为进行开发。这种方法是基于 TDD 的。更准确地说,它使用以清晰的语言(通常是英语)编写的示例,为参与开发的每个人说明系统的行为。我们不会深入研究这个术语,因为它主要影响测试人员和业务分析师。 测试用例- 描述验证被测代码实施所需的步骤、特定条件和参数的脚本。 夹具是成功执行被测方法所必需的测试环境的状态。这是一组预定的对象及其在使用条件下的行为。

    测试阶段

    测试分为三个阶段:
    1. 指定要测试的数据(夹具)。
    2. 使用被测代码(调用被测方法)。
    3. 检查结果并将其与预期结果进行比较。
    关于单元测试:方法、概念、实践 - 7为了确保测试模块化,您需要与应用程序的其他层隔离。这可以使用存根、模拟和间谍来完成。 模拟是可定制的对象(例如,特定于每个测试),并允许您以我们计划接收的响应的形式设置方法调用的期望。期望检查是通过调用 Mock 对象来执行的。 存根- 在测试期间提供对调用的硬连线响应。它们还可以存储有关调用的信息(例如,参数或这些调用的数量)。这些有时被称为他们自己的术语——间谍(Spy)。有时,存根模拟这两个术语会被混淆:区别在于存根不检查任何内容,而仅模拟给定的状态。模拟是一个有期望的对象。例如,必须调用给定的类方法一定次数。换句话说,您的测试永远不会因存根而中断,但可能会因模拟而中断。

    测试环境

    现在我们开始谈正事吧。有多种可用于 Java 的测试环境(框架)。其中最受欢迎的是 JUnit 和 TestNG。为了便于回顾,我们使用: 关于单元测试:方法、概念、实践 - 8JUnit 测试是包含在类中的方法,仅用于测试。类的名称通常与它正在测试的类的名称相同,最后带有+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) - 该方法放置在类之上。这是用于在其中运行测试的按钮。Runner 可以不同:例如有以下几种:MockitoJUnitRunner、JUnitPlatform、SpringRunner 等)。在 JUnit 5 中,@RunWith 注释被更强大的 @ExtendWith 注释取代。让我们看一下比较结果的一些方法:
    • assertEquals(Object expecteds, Object actuals)— 检查传输的对象是否相等。
    • assertTrue(boolean flag)— 检查传递的值是否返回 true。
    • assertFalse(boolean flag)— 检查传递的值是否返回 false。
    • assertNull(Object object)– 检查对象是否为空。
    • assertSame(Object firstObject, Object secondObject)— 检查传递的值是否引用同一个对象。
    • assertThat(T t, Matcher<T> matcher)— 检查 t 是否满足匹配器中指定的条件。
    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 - 设置 moka dao 的行为 5 - 设置我们将在标准之上更新的实例 11 - 使用该方法并获取结果实例 13 - 检查它是否不为零 14 - 检查结果 ID 和指定的方法参数 15 - 检查名称是否已更新 16 - 查看 cpu 17 的结果 - 因为我们没有在更新实例字段中设置它,所以它应该保持不变,让我们检查一下。 关于单元测试:方法、概念、实践 - 9让我们开始吧: 关于单元测试:技术、概念、实践 - 10测试是绿色的,可以呼气了)) 所以,我们总结一下:测试提高了代码的质量,让开发过程更加灵活可靠。想象一下,当我们重新设计具有数百个类文件的软件时,我们需要花费多少精力。一旦我们为所有这些类编写了单元测试,我们就可以充满信心地进行重构。最重要的是,它可以帮助我们轻松发现开发过程中的错误。伙计们,这就是我今天的全部内容:点赞,写评论))) 关于单元测试:方法、概念、实践 - 11
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION