单元测试
Spring Boot 中进行单元测试
在本文中,我们将介绍使用
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.1.6.RELEASE</version>
</dependency>
或者添加
dependencies{
compileOnly('org.projectlombok:lombok')
testCompile('org.springframework.boot:spring-boot-starter-test')
testCompile 'org.junit.jupiter:junit-jupiter-engine:5.2.0'
testCompile('org.mockito:mockito-junit-jupiter:2.23.0')
}
测试相关的注解
在编写
@ExtendWith(SpringExtension.class)
@SpringBootTest
class RegisterUseCaseTest {
@Autowired
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate() {
User user = new User("root", "root@test.com");
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
}
实际上
@RunWith(SpringRunner.class)
@RunWith(SpringJUnit4ClassRunner.class)
其他的注解还包括:
-
@DataJpaTest:提供了持久化层的基础配置,譬如配置
H2 、HSQL 这样的内存数据库、初始化Hibernate 、Spring Data、DataSource,执行EntityScan ,启用SQL 日志等特性。 -
@WebFluxTest: 我们可以使用@WebFluxTest 注解来测试Spring Webflux 控制器。它通常与@MockBean 一起使用,以提供所需依赖项的模拟实现。 -
@JdbcTest: 我们可以使用@JdbcTest 注解来测试JPA 应用程序,但这仅用于需要数据源的测试。注解配置内存中的嵌入式数据库和JdbcTemplate 。 -
@JooqTest: 要测试与jOOQ 相关的测试,我们可以使用@JooqTest 注解,该注解会自动配置DSLContext 。 -
@DataMongoTest: 测试MongoDB 应用程序;默认情况下,如果驱动程序可通过依赖项获得,它将配置内存嵌入式MongoDB ,配置MongoTemplate ,扫描@Document 类,并配置Spring Data MongoDB 存储库。 -
@DataRedisTest: 使测试Redis 应用程序更加容易。它扫描@RedisHash 类并默认配置Spring Data Redis 存储库。 -
@DataLdapTest: 默认情况下,配置内存嵌入式LDAP (如果可用) ,配置LdapTemplate ,扫描@Entry 类,并配置Spring Data LDAP 存储库 -
@RestClientTest: 我们通常使用@RestClientTest 注解来测试REST 客户端。它自动配置不同的依赖项,例如Jackson ,GSON 和Jsonb 支持,配置RestTemplateBuilder ,并默认添加对MockRestServiceServer 的支持。
可被测试的Bean
避免使用@Autowired
@Service
public class RegisterUseCase {
@Autowired
private UserRepository userRepository;
public User registerUser(User user) {
return userRepository.save(user);
}
}
我们无法对此类进行单元测试,因为它无法传递
@Service
public class RegisterUseCase {
private final UserRepository userRepository;
public RegisterUseCase(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User registerUser(User user) {
return userRepository.save(user);
}
}
此版本通过提供允许传入
减少模板代码
这里就是推荐使用
@Service
@RequiredArgsConstructor
public class RegisterUseCase {
private final UserRepository userRepository;
public User registerUser(User user) {
user.setRegistrationDate(LocalDateTime.now());
return userRepository.save(user);
}
}
然后我们可以针对真正有意义地方法进行测试:
class RegisterUseCaseTest {
private UserRepository userRepository = ...;
private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase() {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate() {
User user = new User("zaphod", "zaphod@mail.com");
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
}
DataJpaTest
在单元测试中,我们往往是利用内存数据库进行测试,这里可以参考
@Entity
@Table(name = "person")
public class Employee {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Size(min = 3, max = 20)
private String name;
// standard getters and setters, constructors
}
以及数据持久化层:
@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
public Employee findByName(String name);
}
最后我们的单元测试如下所示:
@RunWith(SpringRunner.class)
@DataJpaTest
public class EmployeeRepositoryIntegrationTest {
@Autowired
private TestEntityManager entityManager;
@Autowired
private EmployeeRepository employeeRepository;
// write test cases here
}
为了执行一些数据库操作,我们需要一些已经在数据库中设置的记录。要设置此数据,我们可以使用
@Test
public void whenFindByName_thenReturnEmployee() {
// given
Employee alex = new Employee("alex");
entityManager.persist(alex);
entityManager.flush();
// when
Employee found = employeeRepository.findByName(alex.getName());
// then
assertThat(found.getName())
.isEqualTo(alex.getName());
}
Mock
很多时候我们的
Mockito
private UserRepository userRepository = Mockito.mock(UserRepository.class);
这将从外部创建一个看起来像assertThat(savedUser.getRegistrationDate()).isNotNull()
处出现userRepository.save()
时返回某些内容。我们使用
@Test
void savedUserHasRegistrationDate() {
User user = new User("zaphod", "zaphod@mail.com");
when(userRepository.save(any(User.class))).then(returnsFirstArg());
User savedUser = registerUseCase.registerUser(user);
assertThat(savedUser.getRegistrationDate()).isNotNull();
}
另一个创建
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
private RegisterUseCase registerUseCase;
@BeforeEach
void initUseCase() {
registerUseCase = new RegisterUseCase(userRepository);
}
@Test
void savedUserHasRegistrationDate() {
// ...
}
}
Mockito.mock()
相同,这取决于使用哪种方式。但是请注意,通过使用
@ExtendWith(MockitoExtension.class)
class RegisterUseCaseTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private RegisterUseCase registerUseCase;
@Test
void savedUserHasRegistrationDate() {
// ...
}
}
MockBean
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeRepository employeeRepository;
@Override
public Employee getEmployeeByName(String name) {
return employeeRepository.findByName(name);
}
}
然后使用
@RunWith(SpringRunner.class)
public class EmployeeServiceImplIntegrationTest {
@TestConfiguration
static class EmployeeServiceImplTestContextConfiguration {
@Bean
public EmployeeService employeeService() {
return new EmployeeServiceImpl();
}
}
@Autowired
private EmployeeService employeeService;
@MockBean
private EmployeeRepository employeeRepository;
// write test cases here
}
这里的
@Before
public void setUp() {
Employee alex = new Employee("alex");
Mockito.when(employeeRepository.findByName(alex.getName()))
.thenReturn(alex);
}
并且设置实际的测试用例:
@Test
public void whenValidName_thenEmployeeShouldBeFound() {
String name = "alex";
Employee found = employeeService.getEmployeeByName(name);
assertThat(found.getName())
.isEqualTo(name);
}
MVC 测试
典型的
@RestController
@RequiredArgsConstructor
class RegisterRestController {
private final RegisterUseCase registerUseCase;
@PostMapping("/forums/{forumId}/register")
UserResource register(
@PathVariable("forumId") Long forumId,
@Valid @RequestBody UserResource userResource,
@RequestParam("sendWelcomeMail") boolean sendWelcomeMail
) {
User user = new User(userResource.getName(), userResource.getEmail());
Long userId = registerUseCase.registerUser(user, sendWelcomeMail);
return new UserResource(userId, user.getName(), user.getEmail());
}
}
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = RegisterRestController.class)
class RegisterRestControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@MockBean
private RegisterUseCase registerUseCase;
@Test
void whenValidInput_thenReturns200() throws Exception {
mockMvc.perform(...);
}
}
请求验证
// Verifying HTTP Request Matching
mockMvc.perform(post("/forums/42/register")
.contentType("application/json"))
.andExpect(status().isOk());
// Verifying Input Serialization
@Test
void whenValidInput_thenReturns200() throws Exception {
UserResource user = new UserResource("Zaphod", "zaphod@galaxy.net");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
.contentType("application/json")
.param("sendWelcomeMail", "true")
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isOk());
}
// Verifying Input Validation
@Value
public class UserResource {
@NotNull
private final String name;
@NotNull
private final String email;
}
@Test
void whenNullValue_thenReturns400() throws Exception {
UserResource user = new UserResource(null, "zaphod@galaxy.net");
mockMvc.perform(post("/forums/{forumId}/register", 42L)
...
.content(objectMapper.writeValueAsString(user)))
.andExpect(status().isBadRequest());
}