单元测试-SpringBoot Test和Mock
“单元测试”
“junit,mock,桩”
1. 什么是单元测试
定义:是指对软件中的最小可测试单元进行检查和验证。
Java里单元指一个方法。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
2. 单元测试与Spring Boot
2.1 引入依赖spring-boot-starter-test
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
spring-boot-starter-test中包含了junit和mockito等依赖
2.2 相关依赖
- junit – 标准的单元测试Java应用程序
- Spring Test & Spring Boot Test – 对Spring Boot应用程序的单元测试提供支持
- Mockito, Java mocking框架,用于模拟任何Spring管理的Bean,比如在单元测试中模拟一个第三方系统Service接口返回的数据,而不会去真正调用第三方系统;
- AssertJ,一个流畅的assertion库,同时也提供了更多的期望值与测试返回值的比较方式;
- JSONassert,对JSON对象或者JSON字符串断言的库。
- …………
2.3 标准的Spring Boot单元测试结构
@DisplayName("AlarmMsgstationController测试类") //起别名
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Test {
}
3. SpringBoot Test常用注解
4. 基本用法
类上添加注解,启动Spring Boot环境
@RunWith(SpringRunner.class)
@SpringBootTest
public class FirstTest {
@Test
public void test() {
int a=1;
Assertions.assertEquals(1,a);//判断二者是否相等
}
}
可以使用Assertions类来判断结果是否符合预期;
4.1 直接注入
对数据访问层(Service层同理)
ps:@Autowired直接注入的方法会真实操作数据库,如果在单元测试中不想改变数据数据库中的值,不能使用直接注入的方法
其实可以在类上再添加这两个注解,通过@Transactional可以知道调用了数据库,对其操作进行回滚
但是如果项目中使用了@Component注解(在SpringBoot项目启动的时候就会跟着实例化/启动),@Component注解的类里有多线程方法,那么在执行单元测试的时候,由于多线程任务的影响,就可能对数据库造成了数据修改,即使使用了事务回滚注解@Transactional。(我在百度上看到的,没找到具体的测试方法,所以没试)
@Component注解:带此注解的类看为组件,当使用基于该注解的配置和类路径扫描的时候,这些类就会被实例化。
@Transactional @Rollback(true) // 事务自动回滚,默认是true。可以不写
4.2 Mock注入
实现原理:使用Stub(桩)技术动态的替换原程序的功能。
直接跑Java代码,不需要启用Spring及连接数据库,模拟一切操作数据库的步骤,不执行任何SQL,也可以模拟任何返回值
4.2.1 使用Mock的优点:
- 可以完全脱离数据库
- 只针对某一个小方法(一个小的单元)来测试,测试过程中,不需要启动其他的东西,不免其他因素可能产生的干扰
4.2.2 编写Mock代码
不再使用@Autowired
启动Spring会导致运行单元测试的时候的速度变慢(run->Junit Test),单元测试只针对某一个类的方法来测试,不需要启动Spring,只需要对应的实体实例就够了,在需要注入bean的时候直接new
不再使用@SpringBootTest
不调用数据库
@Transactional @Rollback(true)这两个注解也不要
使用Assert断言
基本应用:
mock 对象的方法的返回值默认都是返回类型的默认值
import org.junit.Assert;
import org.junit.Test;
import java.util.Random;
import static org.mockito.Mockito.*;
public class MockitoDemo {
@Test
public void test() {
Random mockRandom = mock(Random.class); //mock了一个Random对象
Assert.assertEquals(0, mockRandom.nextInt());//未进行打桩,每次返回值都是0
when(mockRandom.nextInt()).thenReturn(100); // 进行打桩操作,指定调用 nextInt 方法时,永远返回 100
Assert.assertEquals(100, mockRandom.nextInt());
}
}
4.2.3 Mock的注解和常用的方法
@Mock
@Mock 注解可以理解为对 mock 方法的一个替代。使用该注解时,要使用MockitoAnnotations.initMocks
方法,让注解生效。旧版的是initMocks,新版的是openMocks
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import java.util.Random;
import static org.mockito.Mockito.*;
public class MockitoDemo {
@Mock
private Random random;
@Before
public void before() {
// 让注解生效
MockitoAnnotations.initMocks(this);
}
@Test
public void test() {
when(random.nextInt()).thenReturn(100);
Assert.assertEquals(100, random.nextInt());
}
}
也可以用MockitoJUnitRunner
来代替MockitoAnnotations.initMocks
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import java.util.Random;
import static org.mockito.Mockito.*;
@RunWith(MockitoJUnitRunner.class)
public class MockitoDemo {
@Mock
private Random random;
@Test
public void test() {
when(random.nextInt()).thenReturn(100);
Assert.assertEquals(100, random.nextInt());
}
}
@Spy
mock()方法与spy()方法的不同:
- 被spy的对象会走真实的方法,而mock对象不会
- spy方法的参数是对象实例,mock的参数是class
@InjectMocks
mockito 会将 @Mock
、@Spy
修饰的对象自动注入到 @InjectMocks
修饰的对象中
thenReturn
thenReturn 用来指定特定函数和参数调用的返回值;
thenReturn 中可以指定多个返回值。在调用时返回值依次出现。若调用次数超过返回值的数量,再次调用时返回最后一个返回值。
doReturn 的作用和 thenReturn 相同,但使用方式不同:
when(mockRandom.nextInt()).thenReturn(1);//返回值为1
when(mockRandom.nextInt()).thenReturn(1, 2, 3);
doReturn(1).when(random).nextInt();
thenThrow
thenThrow 用来让函数调用抛出异常。(可搭配try catch使用)
thenThrow 中可以指定多个异常。在调用时异常依次出现。若调用次数超过异常的数量,再次调用时抛出最后一个异常。
when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常"));
when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常1"), new RuntimeException("异常2"));
@Test
public void test() {
Random mockRandom = mock(Random.class);
when(mockRandom.nextInt()).thenThrow(new RuntimeException("异常1"), new RuntimeException("异常2"));
try {
mockRandom.nextInt();
Assert.fail();//上一行会抛出异常,到catch中去,走不到这里
} catch (Exception ex) {
Assert.assertTrue(ex instanceof RuntimeException);
Assert.assertEquals("异常1", ex.getMessage());
}
try {
mockRandom.nextInt();
Assert.fail();
} catch (Exception ex) {
Assert.assertTrue(ex instanceof RuntimeException);
Assert.assertEquals("异常2", ex.getMessage());
}
}
对应返回类型是 void 的函数,thenThrow 是无效的,要使用 doThrow。也可以用 doThrow 让返回非void的函数抛出异常
doThrow(new RuntimeException("异常")).when(exampleService).hello();
// 下面这句等同于 when(random.nextInt()).thenThrow(new RuntimeException("异常"));
doThrow(new RuntimeException("异常")).when(random).nextInt();
reset
使用 reset 方法,可以重置之前自定义的返回值和异常。
reset(exampleService);
vetify
使用 verify 可以校验 mock 对象是否发生过某些操作,配合 time 方法,可以校验某些操作发生的次数
//判断backOutstockMapper.selectReportCountByMap()方法是否被调用1次
verify(backOutstockMapper, times(1)).selectReportCountByMap(Mockito.any());
//校验backOutstockMapper.selectReportCountByMap()方法是否被调用过
verify(backOutstockMapper).selectReportCountByMap(Mockito.any());
4.2.5 断言
->assertTrue(String message, boolean condition) 要求condition == true
->assertFalse(String message, boolean condition) 要求condition == false
->assertEquals(String message, XXX expected,XXX actual) 要求expected期望的值能够等于actual
->assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
->assertNotNull(String message, Object object) 要求object!=null
->assertNull(String message, Object object) 要求object==null
->assertSame(String message, Object expected, Object actual) 要求expected == actual
->assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
->assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true
->fail(String message) 要求执行的目标结构必然失败,同样要求代码不可达,即是这个方法在程序运行后不会成功返回,如果成功返回了则报错
4.3 Tips
-
对待类中私有的方法,可以用反射的方式进行测试
-
打包时跳过test
mvn deploy -f pom_http.xml-jar -Dmaven.test.skip=true
-
Mockito 默认是不支持静态方法,可使用 PowerMock 让 Mockito 支持静态方法(新增依赖)
5. 总结
单元测试测试的不是整条业务线,而是类中的单个方法单元。
按照单一性原则的话,一个方法只做一件事,那么针对这个方法的单元测试就简单了。
当多个方法单元测试的结果都没问题的时候,多个方法聚合成的业务链照理说也是没问题的,一个方法中依赖了其他方法的处理结果或返回结果,那么这个结果应当是可预测的,所以也是可以mock出所有场景的,而单元测试也应该覆盖到不同结果对应的场景。
单元测试除了测试代码逻辑外,最大的好处是可以检验整体设计是否合理。一个方法做了太多事的话,就会导致单元测试很难覆盖,比如service层的方法,如果入参的校验,业务逻辑的处理,不同数据表DB的操作,DB返回结果的校验处理全部在单一方法中实现,那对于后期业务的扩展、维护、问题的排查都不好进行;如果把以上说的那些全部分离出来,封装成一个个独立的方法,最后只在一个方法中总调,这样不仅单元测试比较好实现,而且后期的维护,扩展都会很容易。
(ps:上面总结这段话不是我说的,是我在学习的过程中看到一位老哥写在评论区的。领导让我总结一下,我就抄过来敷衍领导了)