三、 Junit5和Mockito
后面讲到的自动生成使用的框架和业界使用最多的都是MocKito,所以这里重点介绍一下,包括使用时遇到的问题。
1. 使用方法
分别单独引入依赖,推荐引入最新版
<!-- junit5 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.7.2</version>
<scope>test</scope>
</dependency>
<!-- mockito -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
<!-- mockito 的junit5适配器 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
使用spring-test全家桶
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.5.0</version>
</dependency>
junit5的使用方法这里就不多做介绍,主要说一下这个ArgumentsProvider接口,实现它就可以自定义参数化类,类似于自带的ValueSource、EnumSource等。
2. Mockito 主要注解介绍
先问为什么,为什么需要Mockito。
因为:现在的java项目几乎离不开spring框架,而其最为著名的就是IOC,所有的bean用容器来管理,所以这给我们单元测试带来一个问题,如果要对bean做单元测试,就需要启动容器,那么带来的时间的开销将会很大。所以Mockito给我门带来了一系列的解决方法,让我们可以轻松的对bean 进行测试。
@Component
public class A {
@Autowired
private B b; // 完全mock
@Autowired
private C c; // 需要执行方法
@Autowired D d; // 需要执行真实方法
public void func(){
}
}
@Component
class C {
@Autowired
private B b;
public void needExec(){
}
}
@Component
public class B {
}
假设我们要对上面的A.func()进行单元测试。
@InjectMocks注解
表示需要注入bean的类,有两种
·被测试类,这种很容易理解,我们测试这个类,当然也需要向其注入bean。比如上面的A
· 被测试类中的,需要执行其真实的方法,但其里面也要主要bean,也就是上面的C,我们需要测试neeExec方法,但我们不关系B的具体细节。现实中比如事物,并发锁等。这一类需要Mockito.spy(new C())的形式,不然会报错
@Mock
表示要mock的数据,也就是不真实执行其方法内容,只按照我们的规则执行,或者返回,比如使用when().thenReturn()语法。
当然也可以,执行真实方法,则需要when().thenCallRealMethod()方式。
@Spy
表示所有方法都走真实方式,比如有些工具类,转换类,我们也写成了bean的形式(严格来说这种需要写成静态工具类)。
@ExtendWith(MockitoExtension.class)
public class ATest {
@InjectMocks
private A a=new A();
@Mock
private B b;
@Spy
private D d;
@InjectMocks
private C c= Mockito.spy(new C());;
@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
}
@ParameterizedTest
@ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
public void funcTest(String str) {
JSONObject arg= TestUtils.getTestArg(str);
a.func();
//todo verify the result
}
}
3. Mockito和junit5常见问题
mock静态方法
mockito3.4以后开始支持,之前的版本可以使用PowerMock辅助使用
Mockito版本和java版本兼容问题
报错如下:
Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.
原因是2.17.0及之前的版本与java8是兼容的。
但2.18之后需要使用java11,为了在java8中使用Mockito,则需要引入另一个包。
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>1.12.6</version>
</dependency>
Jupiter-api版本兼容问题
Process finished with exit code 255
java.lang.NoSuchMethodError: org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances()Lorg/junit/jupiter/api/extension/TestInstance
第一个问题是因为junit5中api、engine、params版本不一致导致的。
第二个问题是因为jupiter-api版本太低的问题,5.7.0以后的版本才支持。
四 、测试代码自动生成
选好了框架,我们还是没有解决我们的问题,“怎么节约开发成本?” ,这一节我们来谈这个问题,这也是我主要想表达的。
对于写单元测试,一直以来是比较头痛的事情,要组装各种各样的数据,可能还没跑成功,就被一堆“xxxx不能为null”的报错搞烦了。因此我们有理由去设想,有没有办法去解决这件事情。
1. 业界和集团方案调研
在做这个事情之前,肯定是要调研有没有现成的框架。答案是有,但很遗憾,没有找到完全契合我想要的效果,我们来看一下这些插件:
public class BaseTest {
protected TestService testService;
public String baseTest() {
return testService.testBase(1); // 4
}
}
public class JCode5 extends BaseTest {
public void testExtend(){
String s = testService.testOther(new Student()); //1
// 调用 另一个方法
System.out.println(testBean());
// 调用基类方法
baseTest();
}
// 使用testService
public String testBean() {
testService.testMuti(new ArrayList<Integer>() {{add(1);}}, 2); //2
return testService.getStr(12); //3
}
/**
* 测试范型类
*/
public void testGeneric(Person person) {
//test
list.stream().forEach(a -> {
System.out.println(a);
});
for (int i = 0; i < 2; i++) {
Long aLong = testService.getLong("1213"
, "12323");
System.out.println(aLong);
}
System.out.println(testBean());
}
}
public class TestService {
public String testBase(Integer integer) {
return "TestBase";
}
public List<String> testMuti(List<Integer> a, Integer c) {
List<String> res = new ArrayList<>();
res.add(a.toString() + c + "test muti");
return res;
}
public String getStr(Integer integer) {
return "TestService" + getInt();
}
public String testOther(Student student) {
return student.getAge() + "age";
}
}
如上,testExtend一共调用了testService的4个方法,我们对比下各个插件生成的代码。
TestMe
@Test
void testTestExtend() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.<String>asList("String"));
when(testService.testOther(any())).thenReturn("testOtherResponse");
jCode5.testExtend(Integer.valueOf(0));
}
@Test
void testTestGeneric() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when(testService.getLong(anyString(), anyString())).thenReturn(Long.valueOf(1));
when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.<String>asList("String"));
jCode5.testGeneric(new Person());
}
生成的代码基本符合逻辑,包括需要mock的bean的逻辑都生成了。
·但它把最重要的一环,也就是数据省略了,只是单纯的用了构造函数的形式。这显然对于我们DDD模型不适应。
· 另外他没用用到junit5的一些特性,比如参数化测试。
· 对于testExtend的方法,它只识别了3个方法。没有识别父类的调用。
JunitGenerate
只能生成基础的框架代码,对于我想mock的逻辑、以及测试方法都没有生成,用处不大。
@Test
public void testTestExtend() throws Exception {
//TODO: Test goes here...
}
Squaretest
生成的方法非常丰富,且一个非常厉害的一点,它能生成多个分支,比如代码逻辑中有if条件,它能生成两个测试,从而走不通的分支。
但是,最大的缺点是“收费软件,不开源”,这就决定了我们没法用它,除非是特别需要。另外测试用过程中还发现了一些其他问题,比如对于继承,重载之类的问题,它解决的也不是很好,往往识别不了需要调用的方法。
虽然无法使用,但还是可以借鉴。
本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理