有了前一章对 Spring 框架依赖注入机制的深入剖析,本章讲解 SpringBoot 框架就显得格外轻松了。同样一个案例,改造为 SpringBoot 项目后需要配置的注解都少了很多,甚至都不需要引入 application-context.xml 文件了;再配合强大的 IDEA 辅助功能,可以真正让开发者聚焦业务逻辑或测试逻辑,而不用过分关注单元测试的环境搭建,非常省心。
第十七章:测试 Spring Boot 应用
本章概要
利用 Spring Initializr 创建项目的方法;
将集成了 JUnit 5 的 Spring 项目迁移为 Spring Boot 项目的方法;
在 Spring Boot 中实现一个测试专用的配置组件;
spring Boot + JUnit 5 实战案例演示。 Working with Spring Boot is like pair-programming with the Spring developers.
用了 Spring Boot 就像在和 Spring 开发者进行结对编程。
------ 佚名
17.1 SpringBoot 简介
Spring Boot 是一个基于 Spring 框架践行 约定优于配置(convention-over-configuration) 原则的成功案例。它极大地减少了初始化 Spring 应用的繁琐配置工作。
上一章提到,Spring 框架通过控制反转和依赖注入,本意是让开发者专注于业务逻辑的实现,但 Spring 繁琐的 XML 配置细节让初学者望而生畏,适得其反。Spring Boot 就是为了简化配置诞生的:大量的默认配置和注解驱动的风格让开发者真正实现了专注于业务本身的开发,很少再花精力去耐心钻研 XML 配置了。
创建一个配置类重新定义 XML 中的两个 Bean 对象,并在类上添加 @TestConfiguration 注解;
在测试类中通过 @Import 注解引入该配置类。
等效的配置类 TestBeans 如下:
java复制代码
@TestConfiguration
public class TestBeans {
@Bean
public Passenger createPassenger(){
Passenger passenger = new Passenger("John Smith");
passenger.setCountry(createCountry());
passenger.setIsRegistered(false);
return passenger;
}
@Bean
public Country createCountry(){
return new Country("USA", "US");
}
}
@Test
@DisplayName("should throw exception if invoked via the name passenger")
void shouldThrowExceptionIfInvokedViaTheNamePassenger() {
final NoSuchBeanDefinitionException ex = assertThrows(NoSuchBeanDefinitionException.class, () -> {
final ApplicationContext context = registrationManager.getApplicationContext();
final Passenger passenger1 = context.getBean("passenger", Passenger.class);
System.out.println("Bean injected by name 'passenger': " + passenger1);
});
assertTrue("No bean named 'passenger' available".contains(ex.getMessage()));
}
@Bean("passenger")
public Passenger createPassenger(){
Passenger passenger = new Passenger("John Smith");
passenger.setCountry(createCountry());
passenger.setIsRegistered(false);
return passenger;
}
这样就能拿到 passenger 对象了:
17.5 SpringBoot 实战:所有乘客注册事件批量响应
本例在上一章 Spring 实战案例的基础上引入了航班实体类 Flight:
java复制代码
public class Flight {
private final String flightNumber;
private final int seats;
private final Set<Passenger> passengers = new HashSet<>();
public Flight(String flightNumber, int seats) {
this.flightNumber = flightNumber;
this.seats = seats;
}
public String getFlightNumber() {
return flightNumber;
}
public int getSeats() {
return seats;
}
public Set<Passenger> getPassengers() {
return Collections.unmodifiableSet(passengers);
}
public boolean addPassenger(Passenger passenger) {
if (passengers.size() >= seats) {
throw new RuntimeException("Cannot add more passengers than the capacity of the flight!");
}
return passengers.add(passenger);
}
public boolean removePassenger(Passenger passenger) {
return passengers.remove(passenger);
}
@Override
public String toString() {
return "Flight " + getFlightNumber();
}
}
@TestConfiguration
public class FlightBuilder {
private static final Map<String, Country> countries = new HashMap<>(){{
put("AU", new Country("Australia", "AU"));
put("US", new Country("USA", "US"));
put("UK", new Country("United Kingdom", "UK"));
}};
@Bean("flight")
Flight buildFlightFromCsv() throws IOException {
Flight flight = new Flight("AA1234", 20);
try(BufferedReader br = new BufferedReader(new FileReader("src/test/resources/flights_information.csv"))) {
String line;
do {
line = br.readLine();
if(line != null) {
String[] fields = line.split(";");
Passenger p = new Passenger(fields[0].trim());
Country c = countries.get(fields[1].trim());
p.setCountry(c);
p.setIsRegistered(false);
flight.addPassenger(p);
}
} while (line != null);
}
return flight;
}
}
最后,通过 @AutoWired 注解向测试类中注入依赖,并完成航班中已注册乘客的批量信息反馈:
java复制代码
@SpringBootTest
@Import(FlightBuilder.class)
public class FlightTest {
@Autowired
private Flight flight;
@Autowired
private RegistrationManager registrationManager;
@Test
void flightTest() {
ApplicationContext ctx = registrationManager.getApplicationContext();
flight.getPassengers()
.parallelStream()
.peek(passenger -> assertFalse(passenger.isRegistered()))
.forEach(passenger -> ctx.publishEvent(new PassengerRegistrationEvent(passenger)));
System.out.println("All passengers from the flight are now confirmed as registered");
flight.getPassengers()
.parallelStream()
.forEach(passenger -> assertTrue(passenger.isRegistered()));
}
}