Sprintboot程序的单元测试和集成测试建议
Springboot测试注解说明
@SpringBootTest
当需要启动完整spring容器的时候考虑使用。
如果没有指定使用注解@ContextConfiguration(loader=...)指定加载,SpringBootContextLoader就是默认的加载器。当没有嵌套使用@Configuration时候,会自动搜索 @SpringBootConfiguration注解。 当我们用@SpringBootConfiguration标记一个类时,意味着该类提供了@Bean定义方法。Spring 容器为我们的应用程序实例化和配置 bean。 @SpringBootConfiguration与@Configuration区别在于@SpringBootConfiguration允许自动发现配置。
@SpringBootTest会首先在当前包中搜索@SpringBootConfiguration,如果没有搜索到则根据包结构向上搜索。
测试类需要与@SpringbootApplication注解标记的类在同一个包内,或者在更下层的包内。(与上句话一致,表达方式不同)
简单的说这个逻辑链条就是:@SpringbootTest去找@SpringbootApplication,@SpringbootApplication注解又包含了SpringBootConfiguration注解。
如果你需要一个跟src/main下代码不一样的应用配置,考虑使用自定义springbootapplication启动类,放在src/test。
@SpringBootTest(classes = CustomApplication.class)
class CustomApplicationTest {
}
2
3
4
- 允许properties自定义环境变量
- 允许从启动参数中定义配置属性
- 提供不同的webEnvironment模式,可以在指定或者随机端口启动运行web server。注册一个TestRestTemplate或者webTestClient用于完整测试webserver。
由于@SpringBootTest会完整的启动容器,建议在集成测试时考虑使用。
@TestPropertySource
指定测试用例要使用的配置,如果不同的测试用例有不同的测试配置,那么可以使用这个注解把配置加载到ApplicationContext中。 例如:
@TestPropertySource("classpath:application.properties")
如果没有指定location,那么默认搜索类名关联的配置进行加载。 如果使用注解测测试类是 is com.example.MyTest, 相应的配置文件就是 "classpath:com/example/MyTest.properties". 如果没有找到默认配置,则抛出IllegalStateException异常。
@ContextConfiguration
加载配置类。如果使用了SpringBootTest,这个注解是不需要的。当你需要Spring容器,又不希望加载全部的类时候,可以考虑用@ContextConfiguration指定加载。某种程度上来说@SpringBootTest(classes="")与@ContextConfiguration(classes="")是等价的。
@Import
@Import与@ContextConfiguration是完全不同的使用场景。不建议互换(有时也不能互换)。 @Import用于一个配置类中,导入其他配置类。例如:
@Configuration
@Import(PersistenceConfig.class)
public class MainConfig {}
2
3
4
比如你禁用了一个包的component scan,那么但是你需要那个包中的一个配置类的时候,你可以考虑用@Import。
@ContextConfiguration只能用于spring测试。
@RunWith(SpringRunner.class)
SpringRunner是SpringJUnit4ClassRunner的别名,所以@RunWith(SpringRunner.class) @RunWith(SpringJUnit4ClassRunner)是等价的。
Junit注解(Junit4)
Junit注解参考Junit说明即可。
Test
Before
After
BeforeClass
AfterClass
Junit5注解
Mockito
@Mock和@MockBean
- @Mock用于不启动容器的单元测试
- @MockBean用于启动部分或者全部容器功能的测试
测试建议
单元测试
重点说三遍:
- 单元测试不要启动容器。
- 单元测试不要启动容器。
- 单元测试不要启动容器。
- dao和service根据情况进行单元测试。如果service仅是返回dao的结果,那么可以仅对dao做单元测试。
- 如果service有业务逻辑,那么建议做单元测试。
- 对于DAO和Service的测试尽可能不要启动容器,freshal-test已经提供了不启动容器的测试mybatis dao测试支持
- 对于service的测试,应该使用mock对象,mock dao层。 例如:
@RunWith(MockitoJUnitRunner.class)
public class TodoListServiceTest {
@InjectMocks
private ToDoService toDoService;
@Mock
private TodoListDao mockDao;
@Test
public void findAllTest() throws Exception {
List<ToDoList> toDoList = new ArrayList<ToDoList>();
toDoList.add(new ToDoList(1L,"jogging at 6:00",true));
toDoList.add(new ToDoList(2L,"meeting at 10:00",true));
when(mockDao.findAll()).thenReturn(toDoList);
List<ToDoList> toDoList2 = toDoService.findAll();
verify(mockDao).findAll();
assertThat(toDoList2).hasSize(2);
}
@Test
public void countTest(){
when(mockDao.count()).thenReturn(2);
long count = toDoService.count();
verify(mockDao).count();
assertThat(count).isEqualTo(2L);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
使用mokcito时,测试用例中verify和assert应该都有。verify确保mock的方法被调用。
- 对于controller层,可以不写单元测试用例,因为集成测试用例也要覆盖到controller层。
- 如果controller层进行单元测试,请使用MockMvc,例如:
@RunWith(MockitoJUnitRunner.class)
public class TodoListControllerStandaloneTest {
@Autowired
MockMvc mockMvc;
@Mock
private ToDoService toDoService;
@InjectMocks
ToDoListController toDoListController;
@Before
public void setup() {
JacksonTester.initFields(this, new ObjectMapper());
// MockMvc standalone approach
mockMvc = MockMvcBuilders.standaloneSetup(toDoListController)
.build();
}
@Test
public void getAllToDos() throws Exception {
List<ToDoList> toDoList = new ArrayList<ToDoList>();
toDoList.add(new ToDoList(1L,"jogging at 6:00",true));
toDoList.add(new ToDoList(2L,"meeting at 10:00",true));
when(toDoService.findAll()).thenReturn(toDoList);
mockMvc.perform(get("/todos")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$",hasSize(2)))
.andDo(print());
verify(toDoService).findAll();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
关于controller测试更多的内容可以参考:https://github.com/mechero/spring-boot-testing-strategies
mybatis单元测试
在写测试用例中,mybatis的测试每次都要启动spring容器,这导致非常耗时。
@MybatisTest注解需要SqlSession和SqlFactory,在使用spring自动配置机制时候,这个由mybatis-spring提供。
在这种情形下,你必须使用spring容器加载mybatis,才可能获得session。为了不启动容器,加速测试用例的运行,建议提供freshal-cloud测试支持
public class DaoWithoutSpringTest {
protected static SqlSessionFactory sqlSessionFactory;
protected static SqlSession session;
public static List<Class<?>> mapperfile = new ArrayList<Class<?>>();
/**
* 返回默认的配置文件,如果文件名称不一样,则在测试用例中复写本方法。
* 仍然使用spring的配置文件,但是不启动spring容器。
* @return
*/
protected static String getPropertyFile(){
return "application.properties";
}
public static void setUpDatabse() throws IOException {
Properties properties = PropertiesLoaderUtils.loadProperties(new ClassPathResource(getPropertyFile()));
String user = properties.getProperty("spring.datasource.username");
String password = properties.getProperty("spring.datasource.password");
String url = properties.getProperty("spring.datasource.url");
String driver = properties.getProperty("spring.datasource.driverClassName");
DataSource dataSource = new org.apache.ibatis.datasource.pooled.PooledDataSource(
driver, url, user, password);
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development",
transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
for(Class clazz: mapperfile){
configuration.addMapper(clazz);
}
sqlSessionFactory = new SqlSessionFactoryBuilder()
.build(configuration);
session = sqlSessionFactory.openSession();
}
@AfterClass
public static void close(){
session.close();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
可以写dao的测试用例如下:
public class TodoDaoWithoutSpringTest extends DaoWithoutSpringTest{
@Test
public void findAllTest() {
//使用session获得dao
TodoListDao todoListDao = session.getMapper(TodoListDao.class);
List<ToDoList> list = todoListDao.findAll();
assertThat(list).hasSize(2);
}
@BeforeClass
public static void setUp() throws IOException {
//添加mapper配置类
mapperfile.add(TodoListDao.class);
//初始化session
setUpDatabse();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
无需mybatis注解,就可以测试dao。
控制台日志显示非常简短
16:16:09.960 [main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter.
16:16:10.172 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection
16:16:10.451 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 132577100.
16:16:10.452 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e6f74c]
16:16:10.457 [main] DEBUG com.freshal.sample.tdd.TodoListDao.findAll - ==> Preparing: select * from todos
16:16:10.511 [main] DEBUG com.freshal.sample.tdd.TodoListDao.findAll - ==> Parameters:
16:16:10.547 [main] DEBUG com.freshal.sample.tdd.TodoListDao.findAll - <== Total: 2
16:16:10.629 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e6f74c]
16:16:10.630 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@7e6f74c]
16:16:10.630 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 132577100 to pool.
2
3
4
5
6
7
8
9
10
上面是用注解方式配置mybatis,xml需要对应修改。
集成测试
- 把@SpringBootTest注解用于集成测试。
- 如果要进行web测试,使用@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
编写测试用例的原则(F.I.R.S.T. principles:)
https://www.appsdeveloperblog.com/the-first-principle-in-unit-testing/
- F - Fast
- I - Independent
- R - Repeatable
- S - Self-Validating
- T - Timely
实践建议:
我们总结一下几个关于springboot测试的实践
- 除了集成测试,避免加载所有的组件。测试什么,加载什么。最好单元测试用例仅有@Test注解。
- 使用Mockito 模拟对象,隔离要被测试的功能。
- 仅加载功能相关的部分。例如不可避免要使用@SpringBootTest测试,那么考虑使用
@SpringBootTest(classes = ABC.class)
这种方式 - RestApi一定要进行集成测试
- 如果使用了JPA,那么用@DataJpaTest注解测试DAO.
- 分层测试,测试用例要小且聚焦功能
- 避免void方法
- 使用@Suite.SuiteClasses组织集成测试套件。比如入库组织一个,出库组织一个属于不同的测试套件。