单元测试
单元测试的目的是保证当前方法正常执行,对软件中的最小可测试单元进行检查和验证,不同于集成测试是将所有模块按照设计要求进行测试,所以工作中单测一般是对业务逻辑(service层)进行测试。
什么是Mock?
在单元测试中,我们往往想去独立地去测一个类中的某个方法,但是这个类可不是独立的,它会去调用一些其它类的方法、dao、service等,这也就导致了以下两个问题:
- 外部服务可能无法在单元测试的环境中正常工作,因为它们可能需要访问数据库或者使用一些其它的外部系统。
- 我们的测试关注点在于这个类的实现上,外部类的一些行为可能会影响到我们对本类的测试,那也就失去了我们进行单测的意义。
这个时候Mockito和PowerMock诞生了,这两种工具都可以用一个虚拟的对象来替换那些外部的、不容易构造的对象,便于我们进行单元测试。两者的不同点在于Mockito对于大多数的标准单测case都很适用,而PowerMock可以去解决一些更难的问题(比如静态方法、私有方法等)
Mock生成的是一个代理对象,默认情况下,执行对象的所有的方法都返回该方法的返回类型的默认值,不会真正去执行该对象的方法。既然这样,那我们在测试中如何使用这个mock出来的对象,来执行方法进行测试呢?这就需要使用到Mockito的Stub(测试桩)来设置Mock对象方法的返回值了。
Mockito
Mockito是一个java测试框架,它提供mock的创建,验证,打桩。在测试中,可能会依赖外部资源,比如数据库等,mockito可以模拟这些数据,进行测试。
比如一个类A,有B、C、D等多个复杂类的成员变量。如果我们测试时候通过
new A()的方式来创建A对象,就需要同时手动创建B、C、D三个对象,并和A关联,提高了测试的复杂性。Mock可以自动生成一个虚拟的A对象,就是帮我们省去了这些复杂的创建流程。参见代码:MyToolService1Test.java、MyFirewallServiceTest.java
package cn.mucang.smart.advert.ops.service; import com.google.common.base.Joiner; import com.google.common.collect.Lists; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Service; import java.util.ArrayList; import java.util.List; /** * @author xu * @Date 2021/6/17 18:32 */ @Service public class MyToolService { String name; public String findById(int id) { return "123"; } public List<String> findAll(int id) { return Lists.newArrayList("1", "2", "3"); } public int getIdValue(String str) { if (!checkIds(str)) { return 0; } int id = Integer.parseInt(str); if (checkIdRange(id)) { return id; } return 0; } private boolean checkIds(String ids) { if (StringUtils.isBlank(ids)) { return false; } if (ids.split(",").length > 1) { return true; } return false; } public static boolean checkIdRange(int id) { if (id > 100 || id < 0) { return false; } return true; } private final boolean finalMethod(String ids) { if (StringUtils.isBlank(ids)) { return false; } return ids.contains("-"); } public String convertStr(String str) { if (finalMethod(str)) { return str.toUpperCase(); } return str.toLowerCase(); } public void quantity(int count) { List<String> arrayList = new ArrayList<>(); for (int i = 0; i < count; i++) { arrayList.add("v-" + i); } System.out.println(Joiner.on(",").join(arrayList)); } public void execQuantity(String ids) { if (checkIds(ids)) { dataLog(ids); return; } name = "ABC"; System.out.println(name); } private void dataLog(String ids) { System.out.println(ids + ",data log...."); } private String checkPrivate(String str) { if (checkIds(str)) { str = "OK"; return str; } str = "NO"; return str; } }
import cn.mucang.smart.advert.ops.service.MyToolService; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; /** * @author xu * @Date 2021/6/28 14:59 */ @RunWith(MockitoJUnitRunner.class) public class MyToolService1Test { // Mockito是一个java测试框架,它提供mock的创建,验证,打桩。在测试中,可能会依赖外部资源,比如数据库等,mockito可以模拟这些数据,进行测试 // 单元测试的目的是保证当前方法正常执行 //Mockito提供了两种Mock的方式:1、注解2、实时调Mockito函数生成 @Mock //注解的方式生成的是一个代理对象 private MyToolService myToolService; //Mock 和 Spy 生成类区别 // Mock:生成的类,所有方法都不是真实的方法,而且返回值都是NULL; 通过when 指定行为。 // Spy:生成的类,所有方法都是真实方法,返回值都是和真实方法一样的;通过when 来将某个真实的方法,替换为指定行为的执行 @Test public void findByIdTest() { //Mock生成的是一个代理对象,默认情况下,执行对象的所有的方法都返回该方法的返回类型的默认值,不会真正去执行该对象的方法。 System.out.println(myToolService.findById(1)); //Mockito.mock函数生成 MyToolService mock = Mockito.mock(MyToolService.class); System.out.println(mock.findById(1)); } @Test public void findAllTest() { System.out.println(myToolService.findAll(1)); } //如果想让mock出来的对象去执行真实的方法,那就得用另一种方式了,spy @Test public void quantityTest() { //spy会去真实的执行对象的方法 MyToolService myToolService = Mockito.spy(new MyToolService()); myToolService.quantity(5); } }
package cn.mucang.smart.advert.ops.service; import cn.mucang.simple.db.condition.SqlPath; import cn.mucang.smart.advert.ops.common.utils.CollectionUtils; import cn.mucang.smart.advert.ops.dao.FirewallEntityDao; import cn.mucang.smart.advert.ops.entity.FirewallEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; /** * @author xu * @Date 2021/7/1 15:01 */ @Service public class MyFirewallService { @Autowired private FirewallEntityDao firewallEntityDao; public List<FirewallEntity> filterByStatus() { SqlPath sqlPath = SqlPath.orderBy(); sqlPath.asc(FirewallEntity.Fields.id); List<FirewallEntity> firewallEntityList = firewallEntityDao.findAll(sqlPath); if (CollectionUtils.isEmpty(firewallEntityList)) { return Collections.emptyList(); } return firewallEntityList.stream().filter(x -> x.getStatus() > 0).collect(Collectors.toList()); } public List<FirewallEntity> filterByStatus1() { SqlPath sqlPath = SqlPath.orderBy(); sqlPath.asc(FirewallEntity.Fields.id); List<FirewallEntity> firewallEntityList = firewallEntityDao.findAll(sqlPath); if (CollectionUtils.isEmpty(firewallEntityList)) { return Collections.emptyList(); } return firewallEntityList.stream().filter(x -> x.getStatus() > 0).collect(Collectors.toList()); } }
import cn.mucang.simple.db.condition.SqlPath; import cn.mucang.smart.advert.ops.dao.FirewallEntityDao; import cn.mucang.smart.advert.ops.entity.FirewallEntity; import cn.mucang.smart.advert.ops.service.MyFirewallService; import com.google.common.collect.Lists; import org.apache.commons.collections.CollectionUtils; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; import java.util.Collections; import java.util.List; /** * @author xu * @Date 2021/7/1 15:09 */ @RunWith(MockitoJUnitRunner.class) public class MyFirewallServiceTest { @Mock private FirewallEntityDao firewallEntityDao; //创建一个实例,简单的说是这个Mock可以调用真实代码的方法,其余用@Mock(或@Spy)注解创建的mock将被注入到用该实例中。 @InjectMocks private MyFirewallService myFirewallService; @Test public void filterByStatus_empty_Test() { //打桩 //mock出firewallEntityDao.findAll返回空的结果, Mockito.when(firewallEntityDao.findAll(Mockito.any(SqlPath.class))).thenReturn(Collections.emptyList()); List<FirewallEntity> firewallEntityList = myFirewallService.filterByStatus(); Assert.assertTrue(CollectionUtils.isEmpty(firewallEntityList)); } @Test public void filterByStatus_Test() { //mock出构造的数据 Mockito.when(firewallEntityDao.findAll(Mockito.any(SqlPath.class))).thenReturn(generateFirewallData()); List<FirewallEntity> firewallEntityList = myFirewallService.filterByStatus(); Assert.assertTrue(firewallEntityList.get(0).getId() == 1); } private List<FirewallEntity> generateFirewallData() { FirewallEntity firewallEntity = new FirewallEntity(); firewallEntity.setId(1L); firewallEntity.setStatus(1); FirewallEntity firewallEntity1 = new FirewallEntity(); firewallEntity1.setId(2L); firewallEntity1.setStatus(0); return Lists.newArrayList(firewallEntity, firewallEntity1); } }
PowerMock
工作直接用powerMock就可以解决所有问题了,即可用mockito也可用powermockito。
有关powermocktito的说明
https://github.com/powermock/powermock/wiki/mockito#introduction
PowerMock是Java开发中的一种Mock框架,用于单元模块测试。
PowerMock可以实现完成对private/static/final方法的Mock(模拟),而Mockito可以对普通的方法进行Mock,如:public等。
PowerMock基本上包含了所有Mockito不能支持的case(大多数情况也就是静态方法,但其实也可以支持私有方法和构造函数的调用)。PowerMock使用了字节码操作,因此它是自带Junit runner的。在使用PowerMock时,必须使用@PrepareForTest注释被测类,mock才会被执行:
PowerMock基于Mockito开发,起语法规则与Mockito一致,主要区别在于使用方面,以实现完成对private/static/final等方法(也支持mock的对象是在方法内部new出来的)的Mock(模拟)
参见代码:MyToolServiceTest.java
import cn.mucang.smart.advert.ops.service.MyToolService; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; import org.powermock.core.classloader.annotations.PrepareForTest; import org.powermock.modules.junit4.PowerMockRunner; import org.powermock.reflect.Whitebox; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; /** * @author xu * @Date 2021/6/28 14:59 */ @RunWith(PowerMockRunner.class) //告诉JUnit使用PowerMockRunner进行测试 @PrepareForTest({MyToolService.class}) //所有需要测试的类列在此处,适用于模拟final类或有final, private, static, native方法的类 public class MyToolServiceTest { //powermockito private方法 @Test public void getEntity_getIdValue_return_false_test() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了false PowerMockito.doReturn(false).when(myToolService, "checkIds", Mockito.anyString()); int idValue = myToolService.getIdValue(Mockito.anyString()); Assert.assertEquals(0, idValue); } //powerMockito private方法 @Test public void getEntity_getIdValue_return_true_test() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了true PowerMockito.doReturn(true).when(myToolService, "checkIds", Mockito.anyString()); int idValue = myToolService.getIdValue("2"); Assert.assertEquals(2, idValue); } //powerMockito 执行static方法 @Test public void getEntity_check_id_range_return_false_test() throws Exception { PowerMockito.mockStatic(MyToolService.class); //没有去真实的执行静态的checkIdRange,但是按我们的要求,直接返回了false Mockito.when(MyToolService.checkIdRange(Mockito.anyInt())).thenReturn(false); MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了true PowerMockito.doReturn(true).when(myToolService, "checkIds", Mockito.anyString()); int idValue = myToolService.getIdValue("2"); Assert.assertEquals(0, idValue); } @Test public void finalMethodTest() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的finalMethods,但是按我们的要求,直接返回了true PowerMockito.doReturn(true).when(myToolService, "finalMethod", Mockito.anyString()); String result = myToolService.convertStr("abc"); Assert.assertEquals(result, "ABC"); } @Test public void finalMethod_lower_Test() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的finalMethods,但是按我们的要求,直接返回了true PowerMockito.doReturn(false).when(myToolService, "finalMethod", Mockito.anyString()); String result = myToolService.convertStr("abc"); Assert.assertEquals(result, "abc"); } //对于一些没有返回值 的方法,如何判断 //判断某个方法执行过了,执行了几次 //判断类成员变量是否做了更改。 @Test public void quantityTest() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了true PowerMockito.doReturn(true).when(myToolService, "checkIds", Mockito.anyString()); myToolService.execQuantity(Mockito.anyString()); //断言 dataLog 被执行一次 PowerMockito.verifyPrivate(myToolService, Mockito.times(1)).invoke("dataLog", Mockito.anyString()); //重新打桩 //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了false PowerMockito.doReturn(false).when(myToolService, "checkIds", Mockito.anyString()); //再次执行 myToolService.execQuantity(Mockito.anyString()); //白盒,获取修改的成员变量 String newStr = (String) Whitebox.getField(MyToolService.class, "name").get(myToolService); Assert.assertTrue("ABC".equals(newStr)); } //查找私有方法,并执行之 @Test public void privateTest() throws InvocationTargetException, IllegalAccessException { MyToolService myToolService = PowerMockito.spy(new MyToolService()); Method checkIds = PowerMockito.method(MyToolService.class, "checkIds", String.class); Object invoke = checkIds.invoke(myToolService, "121,363"); System.out.println(invoke); } //private String checkPrivate(String str) { //私有方法嵌套,则mock之 //查找私有方法,并执行之 @Test public void privateTest1() throws Exception { MyToolService myToolService = PowerMockito.spy(new MyToolService()); Method checkIds = PowerMockito.method(MyToolService.class, "checkPrivate", String.class); //没有去真实的执行私有的checkIds,但是按我们的要求,直接返回了true PowerMockito.doReturn(true).when(myToolService, "checkIds", Mockito.anyString()); Object invoke = checkIds.invoke(myToolService, "121"); System.out.println(invoke); } //关于抽象类的模拟,注意第二个参数 // //mSpyAbstractActivity = Mockito.mock(AbstractActivity.class, Mockito.CALLS_REAL_METHODS) }
Mockito 和 PowerMock的实现原理
- Mockito:通过代理(bytebuddy动态生成匿名子类)实现类功能的模拟。
- PowerMock:通过修改字节码实现类功能的模拟。
maven引用方式:
<powermock.version>2.0.9</powermock.version>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>${powermock.version}</version>
<scope>test</scope>
</dependency>
解决报PowerMockito.mockStatic Argument should be a mock, but is: class java.lang.Class的异常,是版本不兼容导致,可强制用下方版本解决。
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.23.4</version>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>2.0.7</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito2</artifactId>
<version>2.0.2</version>
</dependency>
浙公网安备 33010602011771号