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())
)。但在本例中,id
和description
的值不可预测。只要其他四个字段(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. 总结
使用单次断言验证多个属性能显著提升测试代码质量:
- ✅ 提高可读性
- ✅ 减少出错概率
- ✅ 增强可维护性
本文介绍了三种实现方案:
- JUnit5 –
assertAll()
- AssertJ –
extracting()
+containsExactly()
- AssertJ –
returns()
/doesNotReturn()
+from()
实际开发中,AssertJ的方案更推荐使用,尤其是
returns()
+from()
组合,既安全又优雅。踩坑提醒:避免使用字符串形式的属性名,改用方法引用防止重构遗漏。
所有示例代码可在GitHub仓库查看。