每个开发人员都写过很多代码、函数,但是你能保证你写的每个函数都能执行并且正常吗?
我们太多时间站在功能需求的角度来审视我们的代码,认为需求实现功能逻辑正常,我们就完成了自己的使命。功能逻辑固然重要这个也是我们的目标。但是仅此而已吗,首先作为开发人员要知道,代码的终极目标有两个:实现需求保证逻辑正常、保证代码质量和可维护性。测试人员只能帮助我们查漏需求是否完整实现,对于代码质量和可维护性是需开发自己保证的,所以单元测试必不可少。
JUnit
测试驱动开发,所谓测试驱动开发,就是先写接口- >在写测试->写实现->运行测试。当然这是一种理想情况,大多数我们在开发中还是先写实现,后写测试代码。
- 避免为单元测试写测试,单元测试必须非常简单
- 单元测试不能相互依赖,可以独立运行
- 除了必要的覆盖测试用例,还要注意一些临界值 比如:null、0、“” 等
JUnit 5的使用:
maven依赖:
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
1、 断言
public Integer add(int a, int b){
return a + b;
}
//对add方法的单元测试
@Test
public void testAdd() {
assertEquals(3, add(2, 1));
assertEquals(0, add(0, 0));
assertEquals(0, add(1, -1));
}
断言的方法:assertTrue()、assertFalse()、assertNull()、assertNotNull()、assertEquals()、assertNotEquals()\assertArrayEquals()、assertThrows() ......
注意:测试异常使用assertThrows
2、 初始化资源
| 方法注解 | 描述 |
|---|---|
| @BeforeEach | 单个方法之前 |
| @BeforeAll | 所有测试方法之前 |
| @AfterEach | 单个测试方法之后 |
| @AfterAll | 所有测试方法之后 |
3、 条件测试
@Test
@EnabledIfEnvironmentVariable(named = "DEBUG", matches = "true")
public void testAddOnlyDebug() {
assertEquals(3, add(2, 1));
assertEquals(0, add(0, 0));
assertEquals(0, add(1, -1));
}
只有 DEBUG = true时才会执行
其他条件判断注解:
- @EnabledIf 可以执行任何java语句,比如:@EnabledIf("System.currentTimeMillis()>1573044591641")
其他不经常使用:
- @DisabledOnOs:操作系统有关
- @DisabledOnJre:jre环境有关
4、 参数化测试
@ParameterizedTest
@MethodSource
public void testAdd(int expect, int val1, int val2) {
assertEquals(expect, add(val1, val2));
}
public Integer add(int a, int b) {
return a + b;
}
static List<Arguments> testAdd() {
return List.of(Arguments.arguments(3, 2, 1), Arguments.arguments(2, 1, 1));
}
- @MethodSource 使用方法传参数
- @ValueSource 直接将参数显示 @ValueSource(ints = { -1, -5, -100 })
- @CsvSource 每一个字符串表示一行,一行包含的若干参数用,分隔。
比如:@CsvSource({ "abc, Abc", "APPLE, Apple", "gooD, Good" }) - @CsvFileSource 单独的csv文件提供
@CsvFileSource(resources = { "/test.csv" })
Mock
引入maven依赖
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-all</artifactId>
<version>1.10.19</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.9</version>
<scope>test</scope>
</dependency>
<!-- build tool -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
<scope>provided</scope>
</dependency>
mock 一个简单对象
@Getter
@Setter
public class Event {
private String name;
private String type;
}
@Test
public void mockData(){
//模拟对象
Event event = Mockito.mock(Event.class);
//当调用 event.getName() 时都返回name1
//thenReturn 相似用法还有 thenThrow()、thenAnswer()、thenCallRealMethod()
Mockito.when(event.getName()).thenReturn("name1");
//断言判断
assertEquals("name1",event.getName());
}
当调用event.getName() 时返回 “name1”
Mockito 常用 API :
- verify() 校验方法是否被调用
- doThrow() 模拟抛出异常
doThrow(new RuntimeException()).when(event).getName(); 当调用 event.getName() 时抛出RuntimeException - doAnswer()
doAnswer(new Answer() { @Override public Object answer(InvocationOnMock invocation) throws Throwable { Object[] arguments = invocation.getArguments(); String name = (String) arguments[0]; if ("name".equals(name)){ return name; }else{ throw new RuntimeException(); } } }).when(event).setName(anyString()); event.setName("name"); 当调用 event.setName("name") 只有参数是“name”时通过,其他值抛出异常 - doNothing()
doNothing().when(event).setName("name");
event.setName("name");
什么都不做
还有一种情况:
doNothing().doThrow(new RuntimeException()).when(event).setName("name");
event.setName("name");
event.setName("name");
第一次什么都不做,第二次抛出异常
- doReturn()
List list = new ArrayList<String>();
//Mockito.spy(Object) 用spy监控真实对象,设置真实对象行为
List spy = spy(list);
Mockito.when(spy.get(0)).thenReturn("hello");
//当调用spy.get(0)时会调用真实对象的get(0)函数,此时会发生IndexOutOfBoundsException异常,因为真实List对象是空的
//所以需要doReturn
doReturn("hello").when(spy).get(0);
- doCallRealMethod()
Event mock = mock(Event.class);
doCallRealMethod().when(mock).getString("name");
// 调用event.getString()的真实现
mock.getString("event");
- inOrder()
Event mock = mock(Event.class);
mock.setName("name");
mock.setName("event");
InOrder inOrder = inOrder(mock);
inOrder.verify(mock).setName("name");
inOrder.verify(mock).setName("event");
// 验证mock.setName("name"); mock.setName("event"); 执行顺序,
/**inOrder.verify(mock).setName("event");
inOrder.verify(mock).setName("name");**/
//报错
- times()
//验证 getName调用次数 是否2次
verify(mock, times(2)).getName();
- only()
//只被调用一次
verify(mock, only()).getName();
简化Mock的写法: @Mock private Event event;
Spring-test
maven依赖
<!-- 版本号和你spring的保持一致 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<scope>test</scope>
</dependency>
Demo
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = EventServiceTest.EventServiceConfig.class)
public class EventServiceTest {
@Autowired
private EventService eventService;
/**
* 测试 eventService.findEventById 方法
*/
@Test
public void findEventById(){
Assert.assertNotNull(eventService.findById(2));
Assert.assertEquals(eventService.findById(2).getAppCode(),"appCode");
}
@Configuration
static class EventServiceConfig {
@Bean
public EventService eventService(EventMapper eventMapper){
EventServiceImpl eventService = new EventServiceImpl();
eventService.setEventMapper(eventMapper);
return eventService;
}
@Bean
public EventMapper eventMapper(){
//mock EventMapper
EventMapper mock = Mockito.mock(EventMapper.class);
Event event = new Event();
event.setAppCode("appCode");
//当执行EventMapper.selectByPrimaryKey方法时返回 event
Mockito.when(mock.selectByPrimaryKey(Mockito.any())).thenReturn(event);
return mock;
}
}
}
@RunWith JUnit提供,表示用那种方式来执行这个测试,SpringRunner 由Spring-test提供
@ContextConfiguration 配置Spring容器的配置
上面栗子 是为了测试 eventService.findEventById 方法,发现eventService和依赖EventMapper都是由Spring 容器注入,使用spring-test提供的测试。
将依赖的EventMapper依赖Mock,因为我们主要测试的是eventService.findEventById的逻辑。而且不要使用公共配置,保持每个单元测试之间相互独立,在测试时依赖的Bean越多说明逻辑越复杂,就需要将代码重构。
Spring-boot-test
maven依赖
<!-- 和你的spring版本保持一致 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-test</artifactId>
<scope>test</scope>
</dependency>
Demo
@RunWith(SpringRunner.class)
@SpringBootTest(classes = EventServiceSpringBootTest.Config.class)
public class EventServiceSpringBootTest {
@MockBean
private EventMapper eventMapper;
@Autowired
private EventService eventService;
@Before
public void init(){
Event event = new Event();
event.setAppCode("appCode");
Mockito.when(eventMapper.selectByPrimaryKey(Mockito.any())).thenReturn(event);
}
@Test
public void selectByIdTest(){
Assert.assertNotNull(eventService.findById(2));
Assert.assertEquals(eventService.findById(2).getAppCode(),"appCode");
}
@Configuration
static class Config {
@Bean
public EventService eventService(EventMapper eventMapper) {
EventServiceImpl eventService = new EventServiceImpl();
eventService.setEventMapper(eventMapper);
return eventService;
}
}
}
这里多加了一个@MockBean 用来帮助mock一个对象。
测试覆盖率
单元测试覆盖率只是一个跑分,这个不是我们最终要追求的目标。还是那句话,做单元测试不仅仅是为了完成政治任务,或者一个好看的报告。做单元测试是为了提升代码的质量和架构,不要为了做单元测试而做单元测试。
IDea工具
右键测试类
image.png
image.png









网友评论