zoukankan      html  css  js  c++  java
  • powermockito单元测试之深入实践

    概述

    由于最近工作需要, 在项目中要做单元测试, 以达到指定的测试用例覆盖率指标。项目中我们引入的powermockito来编写测试用例, JaCoCo来监控单元测试覆盖率。关于框架的选择, 网上讨论mockito和powermockito孰优孰劣的文章众多, 这里就不多做阐述, 读者如有兴趣可自行了解。

    依赖引入

    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4-rule-agent</artifactId>
        <version>1.6.6</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito</artifactId>
        <version>1.6.6</version>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>1.6.6</version>
    </dependency>

    被测试类

    public class PowerMockitoDemo {
    
        @Autowired
        private StudentDao studentDao;
    
        @Autowired
        private TeacherService teacherService;
    
        public void study() {
            //doSomething
        }
    
        private void play(Map<String, Object> project, Person person, int hours) {
            //doSomething
        }
    
        private boolean updateStudentName(String newName) {
            //doSomething
        }
    
        public ServiceResult grantRights(List<String> usernames, String rights, String orderid, String id) {
            String value1 = PropertiesUtil.get("key1");
            String value2 = PropertiesUtil.get("key2");
            studentDao.saveRecord(usernames);//返回值类型void
            Student student = studentDao.getStudentById(id);
            boolean result = this.verifyParams(usernames);
            teacherService.syncDB2Redis(usernames, orderid);
            this.updateOperation(rights);
        }
    
        private boolean verifyParams(List<String> usernames) {
            //doSomething
        }
    
        private void updateOperation(String rights) {
            //doSomething
        }
    }

    测试用例基类

    //@PrepareForTest注解和@RunWith注解需结合使用,单独使用将不起作用
    @RunWith(PowerMockRunner.class)
    @PrepareForTest({RedisUtils.class})
    @SuppressStaticInitializationFor({"com.test.util.RedisUtils", "com.test.util.HttpUtils"})//用于阻止类中的静态代码块执行
    public abstract class BaseTest {
    
        public RedisUtils redisUtils;
    
        @Rule
        public ExpectedException thrown = ExpectedException.none();//断言要抛出的异常
    
        public void setUp() {
            initMocks(this);
    
            PowerMockito.suppress(PowerMockito.constructor(ShardedJedisClientImpl.class, String.class));
            redisUtils = PowerMockito.mock(RedisUtils.class);
            Whitebox.setInternalState(RedisUtils.class, "redisUtil", redisUtils);//给类或实例对象的成员变量设置模拟值,这里是给RedisUtils类中的字段redisUtil设置模拟值
    
            PowerMockito.suppress(PowerMockito.constructor(HttpUtils.class));
            PowerMockito.mockStatic(HttpUtils.class);//mock类中所有静态方法
        }
    
        /**
         * @param instance 真实对象
         * @param methodName 方法名
         * @param args 形参列表
         */
        public Object callPrivateMethod(Object instance, String methodName, Object... args) throws Exception {
            return Whitebox.invokeMethod(instance, methodName, args);//调用私有方法
        }
    
    }

    测试用例

    @PrepareForTest({PowerMockitoDemo.class, PropertiesUtil.class})//此处PowerMockitoDemo被测试类添加到@PrepareForTest注解中, 用于mock其静态、final修饰及私有方法;另外,PropertiesUtil工具类由于不通用,不适合抽取到基类BaseTest中, 可在子类mock
    @SuppressStaticInitializationFor("com.test.util.PropertiesUtil")//用于阻止类中的静态代码块执行
    public class PowerMockitoDemoTest extends BaseTest{
    
        @org.powermock.core.classloader.annotations.Mock
        private StudentDao studentDao;
    
        @org.powermock.core.classloader.annotations.Mock
        private TeacherService teacherService;
    
        @org.powermock.core.classloader.annotations.Mock
        @InjectMocks
        private PowerMockitoDemo powerMockitoDemo;
    
        @Override
        @Before
        public void setUp() {
            super.setUp();
            PowerMockito.suppress(PowerMockito.constructor(PropertiesUtil.class));
            PowerMockito.mockStatic(PropertiesUtil.class);//mock类中所有静态方法
        }
    
        @Test
        public void studyWhenCallSuccessfully() {
            PowerMockito.doCallRealMethod().when(powerMockitoDemo).study();
            //doSomething
    
            powerMockitoDemo.study();
        }
    
        @Test
        public void playWhenCallSuccessfully() {
            Map<String, Object> project = new HashMap<String, Object>();
            Person person = new Person();
            int hours = 8;
    
            PowerMockito.doCallRealMethod().when(powerMockitoDemo, "play", Matchers.anyMapOf(String.class, Object.class), Matchers.any(Person.class), Matchers.anyInt());
            //doSomething
    
            this.callPrivateMethod(powerMockitoDemo, "play", project, person, hours);
        }
    
        @Test
        public void updateStudentNameWhenCallSuccessfully() throws Exception {
            String id = "9527";
    
            PowerMockito.when(powerMockitoDemo, "updateStudentName", Matchers.anyString()).thenCallRealMethod();
            //doSomething
    
            boolean actualResult = this.callPrivateMethod(powerMockitoDemo, "updateStudentName", id);
            Assert.assertTrue(actualResult == true);
        }
    
        @Test
        public void getStudentByIdWhenCallSuccessfully() throws Exception {
            List<String> usernames = new ArrayList<String>();
            String rights = "万叶飞花流";
            String orderid = "orderid";
            String id = "id";
    
            //调用真实方法
            PowerMockito.when(powerMockitoDemo.grantRights(Matchers.anyListOf(String.class), Matchers.anyString(), Matchers.anyString(), Matchers.anyString())).thenCallRealMethod();
            //当方法内重复调用同一个方法时, 可通过Matchers.eq()方法来指定实际入参来加以区分
            PowerMockito.when(PropertiesUtil.get(Matchers.eq("key1"))).thenReturn("value1");
            PowerMockito.when(PropertiesUtil.get(Matchers.eq("key2"))).thenReturn("value2");
            //返回值类型为void,不做任何事情
            PowerMockito.doNothing().when(studentDao).saveRecord(Matchers.anyListOf(String.class));
            //类似需要调用数据库、redis、远程服务的,可直接模拟返回值,不做方法的真实调用
            PowerMockito.when(studentDao.getStudentById(Matchers.anyString())).thenReturn(new Student());
            //调用真实的私有方法
            PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenCallRealMethod();
            //模拟私有方法返回值
            PowerMockito.when(powerMockitoDemo, "verifyParams", Matchers.anyListOf(String.class)).thenReturn(true);
            //模拟方法调用抛出异常。当被调用的方法头没有显式声明异常时, 则mock只支持unchecked exception,比如这里syncDB2Redis()方法签名没有声明任何异常,则thenThrow()模拟异常时只支持模拟运行时异常,使用非运行时异常将编译不通过
            PowerMockito.when(teacherService.syncDB2Redis(Matchers.anyListOf(String.class), Matchers.anyString())).thenThrow(new RuntimeException("Failed to call remote service"));
            PowerMockito.doNothing().when(powerMockitoDemo, "updateOperation", Matchers.anyString());
    
            ServiceResult expectedResult = new ServiceResult();
            ServiceResult actualResult = powerMockitoDemo.grantRights(usernames, rights, orderid, id);
            //断言实际调用结果是否符合预期值
            Assert.assertEquals(JSON.toJSONString(expectedResult), JSON.toJSONString(actualResult));
        }
    }

    如上, 具体的阐释在代码注释中都已经标注, 抽取基类是为了提高代码可重用性。博主这里是为了演示, 所以代码看起来会有点臃肿, 在实际项目使用中, 可以通过静态引入 import static org.mockito.Mockito.when; 和 import static org.mockito.Matchers.*; 来简化代码, 提高可阅读性。

    另外由于被测试类在测试方法中被mock掉, 且被@PrepareForTest注解标记时, JaCoCo工具统计测试覆盖率将忽略该测试类。可通过在基类BaseTest中添加 @Rule public ExpectedException thrown = ExpectedException.none(); , 并去掉 @RunWith(PowerMockRunner.class) 和 @SuppressStaticInitializationFor 来使统计测试覆盖率生效。但这里又有一个新问题产生, 基类修改之后, 发现测试用例无法进入debug调试, 因此建议先用修改前的基类来编写单元测试, 便于调试, 待测试用例完成后, 再修改基类令Jacoco统计覆盖率生效。

    关于PowerMockito的实践, 博主目前在项目中的使用主要就涉及到了这些, 所以这次做了回标题党"XXX深入实践" ^_^, 后面如有接触新的相关知识点, 会陆续更新到本篇文章中。如有错误, 欢迎指正, 谢谢你^_^

    参考资料

    非web下的PowerMockito单元测试

  • 相关阅读:
    SpringBoot(十二)------国际化配置
    SpringBoot(十一) ----SpringBoot结合mybatis实现增删改查
    SpringBoot(十)----SpringMVC自动配置&扩展配置
    SpringBoot学习(九) ------访问静态web资源
    leetcode-----两数相加
    JDBC — 学习大纲
    网络编程
    StringBuffer
    代理
    加载文件的两种方式
  • 原文地址:https://www.cnblogs.com/qingshanli/p/9763130.html
Copyright © 2011-2022 走看看