单元测试
测试独立功能
为了演示创建单元测试的过程,我们设想这样一个功能场景,给定一组 OrderLine
实例,要求计算其关联的 Order
(订单)的总价。
给定一个 OrderAmountCalculation
类专门处理计算。这不是一个 Spring bean,而只是一个普通的 Java 类:
public class OrderAmountCalculation {
public BigDecimal calculateTotalAmount(List<OrderLine> orderLines) {
return orderLines.stream()
.map(this::totalPriceForOrderLine)
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
private BigDecimal totalPriceForOrderLine(OrderLine orderLine) {
BigDecimal productPrice = orderLine.getProduct().getPrice();
BigDecimal quantity = BigDecimal.valueOf(orderLine.getQuantity());
return productPrice.multiply(quantity);
}
}
该功能的单元测试示例:
class OrderAmountCalculationTest {
private OrderAmountCalculation orderAmountCalculation;
private static final BigDecimal USD499 = BigDecimal.valueOf(499.0);
private Product iPad;
@Test
void calculateTotalAmount() {
// given:
orderAmountCalculation = new OrderAmountCalculation(); (1)
// and:
iPad = new Product(); (2)
iPad.setName("Apple iPad");
iPad.setPrice(USD499);
// and:
OrderLine twoIpads = new OrderLine();
twoIpads.setProduct(iPad);
twoIpads.setQuantity(2.0);
// when:
var totalAmount = orderAmountCalculation.calculateTotalAmount(
List.of(twoIpads)
);
// then:
assertThat(totalAmount) (3)
.isEqualByComparingTo(BigDecimal.valueOf(998.0));
}
}
1 | 通过构造函数而非 Srping 初始化一个 OrderAmountCalculation 类实例。 |
2 | 实体通过调用构造函数创建(而非使用 Jmix 的 Metadata API)。 |
3 | 通过 AssertJ 断言判断计算结果。 |
此测试类不包含任何 Spring Boot 测试注解(例如,@SpringBootTest
),因此测试不使用 Spring 上下文,运行速度非常快。但是测试中缺少 Spring 上下文也意味着无法在测试类中使用 @Autowired
来获取 Spring Bean 的实例。如果被测类与 Spring Bean 有任何依赖关系,则必须手动实例化这些依赖关系。
使用 Mockito 模拟
对于单元测试,上述限制是可以接受的,因为测试用例的范围通常是单个类的某些独立功能。
我们来看下面的例子:有一个类调用 Jmix TimeSource
API 来获取当前日期,用于计算特定客户今年已进行的订单数量。
下面是该类的实现:
@Component
public class RecentOrdersCounter {
private final TimeSource timeSource;
public RecentOrdersCounter(TimeSource timeSource) {
this.timeSource = timeSource;
}
public long countFromThisYear(Customer customer) {
return customer.getOrders().stream()
.filter(this::fromThisYear)
.count();
}
private boolean fromThisYear(Order order) {
int thisYear = timeSource.now().toLocalDate().getYear();
return thisYear == order.getDate().getYear();
}
}
该类使用了 @Component
注解,这样 Spring 能自动对其实例化,并支持依赖注入。但是,如果要在单元测试中测试此功能,则需要手动实例化 RecentOrdersCounter
类,并为构造函数提供 TimeSource
的实例。
假设我们要测试 RecentOrdersCounter
的功能,需要测试这样一个场景:
假设我们有两个订单:一个是 2019 年的订单,一个是 2020 年的订单,如果今年是 2020 年,则测试结果应该是会有一个订单。
为此,需要控制 TimeSource
返回的当前时间,并模拟当前年份为 2020 年。
Mockito 是一个支持这种仿真的模拟库。在 Jmix 项目中默认可用。
下面是此测试用例的一个示例:
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
class RecentOrdersCounterTest {
TimeSource timeSourceMock = mock(TimeSource.class); (1)
LocalDateTime MAR_01_2020 = LocalDate.of(2020, 3, 1).atStartOfDay();
@Test
void given_itIs2020_and_customerWithOneOrderIn2020_when_countFromThisYear_then_resultIs1() {
// given:
when(timeSourceMock.now())
.thenReturn(ZonedDateTime.of(MAR_01_2020, ZoneId.systemDefault())); (2)
// and:
RecentOrdersCounter counter = new RecentOrdersCounter(timeSourceMock); (3)
// and:
Customer customer = new Customer();
Order orderFrom2020 = orderWithDate(LocalDate.of(2020, 2, 5));
Order orderFrom2019 = orderWithDate(LocalDate.of(2019, 5, 1));
customer.setOrders(List.of(orderFrom2020, orderFrom2019));
// when:
long recentOrdersCount = counter.countFromThisYear(customer);
// then:
assertThat(recentOrdersCount)
.isEqualTo(1);
}
1 | Mockito.mock() 方法创建一个模拟的实例,可以用于控制该类的行为。 |
2 | 调用 Mockito.when() 定义当 TimeSource 的 now() 方法被调用时,需要返回 ZonedDateTime 类型的 2020-03-01 。 |
3 | 初始化计算类时,构造函数传入的是 TimeSource 的模拟实例。 |
如果需要在单元测试中测试 Spring 组件,请使用基于构造函数的注入,而非 @Autowired 的字段。
|
使用 Mockito 的更多内容,请参阅 Mockito 的文档。
用断言进行验证
可以使用 AssertJ 库进行断言。
AssertJ DSL 提供了用于验证测试结果的流式 API。断言方法(例如,assertThat
)应该从 org.assertj.core.api.Assertions
静态引入,例如:
import static org.assertj.core.api.Assertions.assertThat;
这里是使用 AssertJ 对字符串断言的简单示例:
// given:
String customerName = "Mike Myers";
// expect:
assertThat(customerName)
.startsWith("Mike")
.endsWith("Myers");
请注意,对同一结果对象可以串联多个断言。
如果测试失败,JUnit / AssertJ 能提供关于期望和实际结果之间差异的错误信息:
Expecting actual: "Mike Myers" to end with: "Murphy"
根据对象的类型,AssertJ 提供了不同的断言方法来比较值。例如,在比较列表时,AssertJ 提供了 hasSize
和 contains
方法:
// given:
String bruceWillis = "Bruce Willis";
String mikeMyers = "Mike Myers";
String eddiMurphy = "Eddi Murphy";
// when:
List<String> customers = List.of(mikeMyers, eddiMurphy);
// expect:
assertThat(customers)
.hasSize(2)
.contains(eddiMurphy)
.doesNotContain(bruceWillis);
关于断言的更多内容,请参阅 AssertJ 文档。