1. 概述

作为程序员,我们经常编写测试来确保代码按预期工作。测试中的标准实践之一就是使用断言(assertion)。当需要验证一个对象的多个属性时,传统做法是写一堆断言语句,但这会让代码显得冗长且难以阅读。

本文将探讨如何通过单次断言调用来验证多个属性,让测试代码更简洁优雅。

2. 问题引入

实际开发中,我们经常需要检查对象的多个属性。传统做法是为每个属性编写单独的断言,这会导致代码重复且可读性差。更优雅的方式是使用单次断言调用验证多个属性。

先看一个简单的POJO类作为示例:

class Product {
    private Long id;
    private String name;
    private String description;
    private boolean onSale;
    private BigDecimal price;
    private int stockQuantity;

    // 全参构造器省略
    // getter/setter方法省略
}

Product类包含6个属性。假设我们实现了生成Product实例的程序,通常我们会将生成的实例与预期对象比较(如assertEquals(EXPECTED_PRODUCT, myProgram.createProduct()))。但在本例中,iddescription的值不可预测。只要其他四个字段(name、onSale、price、stockQuantity)符合预期,我们就认为程序正确。

创建预期结果对象:

Product EXPECTED = new Product(42L, "LG Monitor", "32 inches, 4K Resolution, Ideal for programmers", true, new BigDecimal("429.99"), 77);

为简化演示,我们直接创建测试对象:

Product TO_BE_TESTED = new Product(-1L, "LG Monitor", "dummy value: whatever", true, new BigDecimal("429.99"), 77);

接下来看看如何组织断言。

3. 使用JUnit5的assertAll()

JUnit是最流行的单元测试框架之一,JUnit 5带来了许多新特性,其中assertAll()就是解决本问题的利器。

assertAll()接收一组断言,所有断言将在单次调用中执行。 即使多个断言失败,测试也会一次性报告所有错误。

使用assertAll()组合属性断言:

assertAll("Verify Product properties",
  () -> assertEquals(EXPECTED.getName(), TO_BE_TESTED.getName()),
  () -> assertEquals(EXPECTED.isOnSale(), TO_BE_TESTED.isOnSale()),
  () -> assertEquals(EXPECTED.getStockQuantity(), TO_BE_TESTED.getStockQuantity()),
  () -> assertEquals(EXPECTED.getPrice(), TO_BE_TESTED.getPrice()));

虽然实现了目标,但assertAll()内部仍是四个独立的lambda表达式,代码仍显冗长。接下来看更简洁的方案。

4. 使用AssertJ的extracting()和containsExactly()

AssertJ是强大的Java断言库,提供流式API。其extracting()方法可从对象中提取指定属性值,返回一个列表。再配合containsExactly()验证列表值是否完全匹配。

组合使用这两个方法:

assertThat(TO_BE_TESTED)
  .extracting("name", "onSale", "stockQuantity", "price")
  .containsExactly(EXPECTED.getName(), EXPECTED.isOnSale(), EXPECTED.getStockQuantity(), EXPECTED.getPrice());

优点:代码简洁直观
⚠️ 缺点:属性名以字符串形式传递,存在以下风险:

  • 拼写错误不会在编译时发现
  • 重命名属性后测试仍能编译,运行时才会报错

更安全的做法是使用方法引用(method reference)替代字符串:

assertThat(TO_BE_TESTED)
  .extracting(Product::getName, Product::isOnSale, Product::getStockQuantity, Product::getPrice)
  .containsExactly(EXPECTED.getName(), EXPECTED.isOnSale(), EXPECTED.getStockQuantity(), EXPECTED.getPrice());

5. 使用AssertJ的returns()和from()

当需要验证的属性增多时(比如10个以上),extracting()的单行代码会变得难以阅读。AssertJ提供了更优雅的替代方案:returns()from()

returns()验证对象通过指定函数返回的值是否符合预期,语法为:assertToBeTestedObject.returns(Expected, from(FunctionToGetValue))

应用到Product对象:

assertThat(TO_BE_TESTED)
  .returns(EXPECTED.getName(), from(Product::getName))
  .returns(EXPECTED.isOnSale(), from(Product::isOnSale))
  .returns(EXPECTED.getStockQuantity(), from(Product::getStockQuantity))
  .returns(EXPECTED.getPrice(), from(Product::getPrice));

优势

  • 流式API清晰易读
  • 支持任意数量的属性验证
  • 可与doesNotReturn()混合使用

混合验证示例(同时验证匹配和不匹配的属性):

assertThat(TO_BE_TESTED)
  .returns(EXPECTED.getName(), from(Product::getName))
  .returns(EXPECTED.isOnSale(), from(Product::isOnSale))
  .returns(EXPECTED.getStockQuantity(), from(Product::getStockQuantity))
  .returns(EXPECTED.getPrice(), from(Product::getPrice))
  .doesNotReturn(EXPECTED.getId(), from(Product::getId))
  .doesNotReturn(EXPECTED.getDescription(), from(Product::getDescription));

6. 总结

使用单次断言验证多个属性能显著提升测试代码质量:

  • ✅ 提高可读性
  • ✅ 减少出错概率
  • ✅ 增强可维护性

本文介绍了三种实现方案:

  1. JUnit5assertAll()
  2. AssertJextracting() + containsExactly()
  3. AssertJreturns()/doesNotReturn() + from()

实际开发中,AssertJ的方案更推荐使用,尤其是returns()+from()组合,既安全又优雅。踩坑提醒:避免使用字符串形式的属性名,改用方法引用防止重构遗漏。

所有示例代码可在GitHub仓库查看。


原始标题:Single Assert Call for Multiple Properties in Java Unit Testing