Android单元测试(五):PowerMock

作者: 一笑小先生 | 来源:发表于2018-05-31 15:07 被阅读13次
    timg.jpeg

    简介

    PowerMock 也是一个单元测试模拟框架,它是在其它单元测试模拟框架的基础上做出的扩展。通过提供定制的类加载器以及一些字节码篡改技巧的应用,PowerMock 现了对静态方法、构造方法、私有方法以及 Final 方法的模拟支持,对静态初始化过程的移除等强大的功能。因为 PowerMock 在扩展功能时完全采用和被扩展的框架相同的 API, 熟悉 PowerMock 所支持的模拟框架的开发者会发现 PowerMock 非常容易上手。PowerMock 的目的就是在当前已经被大家所熟悉的接口上通过添加极少的方法和注释来实现额外的功能,目前,PowerMock 仅支持 EasyMock 和 Mockito。

    配置

    PowerMock与Mockito有许多的版本兼容问题,主要是两个不同的团队不同的库版本更新进度不同,具体如果同时使用两者,需要参考官方文档说明.

    dependencies {
        testImplementation 'junit:junit:4.12'
        // mockito
        testImplementation 'org.mockito:mockito-core:2.4.0'
        androidTestImplementation 'org.mockito:mockito-core:2.4.0'
        // PowerMock注意版本需要与上面的Mockito版本兼容,否则会造成各类兼容问题
        testImplementation 'org.powermock:powermock-core:1.7.0RC2'
        testImplementation 'org.powermock:powermock-module-junit4:1.7.0RC2'
        testImplementation 'org.powermock:powermock-api-mockito2:1.7.0RC2'
    }
    

    快速入门

    下面创建EmployeeController类用于给Employee类执行Create, Read, Update, and Delete (CRUD)。实际工作由EmployeeService完成。getProjectedEmployeeCount方法预计公司员工每年增加20%,并返回近似取整。

    public class EmployeeController {
    
        private EmployeeService employeeService;
    
        public EmployeeController(EmployeeService employeeService) {
            this.employeeService = employeeService;
        }
    
        public int getProjectedEmployeeCount() {
            final int actualEmployeeCount = employeeService.getEmployeeCount();
            return (int) Math.ceil(actualEmployeeCount * 1.2);
        }
    
        public void saveEmployee(Employee employee) {
            employeeService.saveEmployee(employee);
        }
    }
    
    public class EmployeeService {
    
        public int getEmployeeCount() {
            throw new UnsupportedOperationException();
        }
    
        public void saveEmployee(Employee employee) {
            throw new UnsupportedOperationException();
        }
    }
    
    public class Employee {
    }
    
    

    由于getEmployeeCount等方法没有真正实现,我们需要mock:

    public class EmployeeControllerTest {
    
       @Test
       public void testReturnCountOfEmployees() {
           EmployeeService mock = mock(EmployeeService.class);
           when(mock.getEmployeeCount()).thenReturn(8);
           EmployeeController controller = new EmployeeController(mock);
           assertEquals(10,controller.getProjectedEmployeeCount());
       }
    
       @Test
       public void testSaveEmployee() {
           EmployeeService mock = mock(EmployeeService.class);
           EmployeeController controller = new EmployeeController(mock);
           Employee employee = new Employee();
           controller.saveEmployee(employee);
           verify(mock).saveEmployee(employee);
       }
    }
    

    上面的saveEmployee(Employee)没有返回值,我们只需要用verify确认有调用即可。如果注释掉employeeController.saveEmployee(employee);就会有如下报错:

    Wanted but not invoked:
    employeeService.saveEmployee(
        com.sky.platform.unittest.powermockito.Employee@6e1567f1
    );
    -> at com.sky.platform.powermockito.EmployeeControllerTest.testSaveEmployee(EmployeeControllerTest.java:29)
    Actually, there were zero interactions with this mock.
    
    Wanted but not invoked:
    employeeService.saveEmployee(
        com.sky.platform.unittest.powermockito.Employee@6e1567f1
    );
    -> at com.sky.platform.powermockito.EmployeeControllerTest.testSaveEmployee(EmployeeControllerTest.java:29)
    Actually, there were zero interactions with this mock.
    
    

    模拟static方法

    修改类Employee:

    public class Employee {
    
        public static int count() {
            throw new UnsupportedOperationException();
        }
    }
    

    修改EmployeeService类的方法:

    public class EmployeeService {
    
        public int getEmployeeCount() {
            return Employee.count();
        }
    }
    

    新建EmployeeServiceTest类:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest(Employee.class)
    public class EmployeeServiceTest {
    
        @Test
        public void testStaticMethodReturnTheCountOfEmployees() {
            mockStatic(Employee.class);
            when(Employee.count()).thenReturn(900);
    
            EmployeeService service = new EmployeeService();
            assertEquals(900,service.getEmployeeCount());
        }
    }
    

    @RunWith(PowerMockRunner.class)语句告诉JUnit用PowerMockRunner来执行测试。
    @PrepareForTest(Employee.class)语句告诉PowerMock准备Employee类进行测试。适用于模拟final类或有final, private, static, native方法的类。

    注意这里使用的是mockStatic而不是上面的mock.

    下面我们模拟下返回void的静态方法。在Employee添加加薪方法:

     public static void giveIncrementOf(int percentage) {
            throw new UnsupportedOperationException();
        }
    
        @Test
        public void testShouldReturnTrueWhenIncrementOf10Percent() {
            mockStatic(Employee.class);
            doNothing().when(Employee.class);
            Employee.giveIncrementOf(10);
            EmployeeService employeeService = new EmployeeService();
            assertTrue(employeeService.giveIncrementToAllEmployeesOf(10));
    
        }
    
        @Test
        public void testShouldReturnFalseWhenIncrementOf10Percent() {
            mockStatic(Employee.class);
            doThrow(new IllegalStateException()).when(Employee.class);
            Employee.giveIncrementOf(10);
            EmployeeService employeeService = new EmployeeService();
            assertFalse(employeeService.giveIncrementToAllEmployeesOf(10));
        }
    

    PowerMockito.doNothing方法告诉PowerMock下一个方法调用时什么也不做。

    PowerMockito.doThrow方法告诉PowerMock下一个方法调用时产生异常。

    PowerMock使用自定义类加载器和字节码操作来模拟静态方法。对于实例中没有mock的方法,也有默认返回值,比如返回int类型的方法,默认返回0。

    PowerMockito.doNothing和PowerMockito.doThrow的语法可用于实例方法:

    先在Employee类添加方法save:

     public void save() {
         throw new UnsupportedOperationException();
     }
    

    创建测试EmployeeTest 类:

    public class EmployeeTest {
    
        @Test
        public void testShouldNotDoAnythingIfEmployeeWasSaved() {
            Employee employee = PowerMockito.mock(Employee.class);
            PowerMockito.doNothing().when(employee).save();
            try {
                employee.save();
            } catch (Exception e) {
                fail("Should not have thrown an exception");
            }
        }
    
        @Test(expected = IllegalStateException.class)
        public void testShouldThrowAnExceptionIfEmployeeWasNotSaved() {
    
            Employee employee = PowerMockito.mock(Employee.class);
            PowerMockito.doThrow(new IllegalStateException()).when(employee).save();
            employee.save();
        }
    }
    

    注意这里doThrow和doNothing方法不会对下一行产生影响。

    验证方法调用

    修改Employee类,新增如下方法:

        public boolean isNew() {
            throw new UnsupportedOperationException();
        }
    
        public void update() {
            throw new UnsupportedOperationException();
        }
    
        public void create() {
            throw new UnsupportedOperationException();
        }
    

    修改EmployeeService类的saveEmployee方法:

        public void saveEmployee(Employee employee) {
            if(employee.isNew()) {
                employee.create();
                return;
            }
            employee.update();
        }
    

    Mockito.verify(mock).create()验证调用了create方法。 Mockito.verify(mock, Mockito.never()).update();验证没有调用update方法

    验证静态方法调用

        @Test
        public void testInvoke_giveIncrementOfMethodOnEmployee() {
            mockStatic(Employee.class);
    
            EmployeeService service = new EmployeeService();
            service.giveIncrementToAllEmployeesOf(9);
    
            // 验证方法被调用
            verifyStatic();
            // 验证调用的所属静态方法
            Employee.giveIncrementOf(9);
        }
    

    其他验证模式可以验证调用次数:

    • Mockito.times(int n) : 确切调用了几次
    • Mockito.atLeastOnce() : 至少调用一次
    • Mockito.atLeast(int n) : 至少调用几次
    • Mockito.atMost(int n) : 最多调用几次

    模拟final类或方法

    新增EmployeeIdGenerator类:

    public final class EmployeeIdGenerator {
    
        public static int getNextId() {
            return 0;
        }
    }
    
    

    修改EmployeeServiceTest测试类,头部注解修改增加final类,增加测试方法:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({Employee.class, EmployeeIdGenerator.class})
    
    
        @Test
        public void testShouldGenerateEmployeeIdIfEmployeeIsNew() {
            Employee mockEmployee = mock(Employee.class);
            when(mockEmployee.isNew()).thenReturn(true);
    
            mockStatic(EmployeeIdGenerator.class);
            when(EmployeeIdGenerator.getNextId()).thenReturn(90);
            EmployeeService employeeService = new EmployeeService();
            employeeService.saveEmployee(mockEmployee);
    
            verifyStatic();
            Assert.assertEquals(0,EmployeeIdGenerator.getNextId());
            Mockito.verify(mockEmployee).setEmployeeId(90);
            Mockito.verify(mockEmployee).create();
        }
    
    

    可见final和static的在类头部处理方法类似,两者可以基本归为一类。

    其他java mock框架大多基于代理模式,参见https://en.wikipedia.org/wiki/Proxy_pattern#Example 。这种方式严重依赖子类及方法可以重载。所以不能模拟final和static。

    模拟构造方法

    现在创建新职员的时候要发送欢迎邮件

    新增类WelcomeEmail:

    public class WelcomeEmail {
    
        public WelcomeEmail(Employee employee, String message) {
        }
    
        public void send() {
            throw new UnsupportedOperationException();
        }
    }
    

    修改EmployeeService类的saveEmployee方法:

        public void saveEmployee(Employee employee) {
            if(employee.isNew()) {
                employee.create();
                employee.setEmployeeId(EmployeeIdGenerator.getNextId());
                WelcomeEmail email = new WelcomeEmail(employee,"Hello World");
                email.send();
                return;
            }
            employee.update();
        }
    

    修改EmployeeServiceTest测试类:

    @RunWith(PowerMockRunner.class)
    @PrepareForTest({EmployeeService.class,EmployeeIdGenerator.class})
    
    
    
        @Test
        public void testShouldSendWelcomeEmailToNewEmployees() throws Exception {
            Employee mockEmployee = mock(Employee.class);
            when(mockEmployee.isNew()).thenReturn(true);
    
            mockStatic(EmployeeIdGenerator.class);
    
            WelcomeEmail mockEmail = mock(WelcomeEmail.class);
            whenNew(WelcomeEmail.class).withArguments(mockEmployee, "Hello World").thenReturn(mockEmail);
    
            EmployeeService service = new EmployeeService();
            service.saveEmployee(mockEmployee);
    
            verifyNew(WelcomeEmail.class).withArguments(mockEmployee, "Hello World");
            Mockito.verify(mockEmail).send();
        }
    
    

    注意PowerMockito.verifyNew的第2个参数支持前面提到的验证模式。PowerMockito.whenNew().withArguments(...).thenReturn()是对构造方法的mock模式,PowerMockito.verifyNew().withArguments()是验证模式。

    使用spy进行部分模拟

    现在调整类EmployeeService,拆分saveEmployee为方法:saveEmployee和createEmployee:

        public void saveEmployee(Employee employee) {
            if(employee.isNew()) {
                createEmployee(employee);
                return;
            }
            employee.update();
        }
    
        void createEmployee(Employee employee) {
            employee.setEmployeeId(EmployeeIdGenerator.getNextId());
            employee.create();
            WelcomeEmail email = new WelcomeEmail(employee,"Hello World");
            email.send();
        }
    

    EmployeeServiceTest类添加测试方法:

        @Test
        public void testInvokeTheCreateEmployeeMethod() {
            EmployeeService spy = spy(new EmployeeService());
            Employee mockEmployee = mock(Employee.class);
    
            when(mockEmployee.isNew()).thenReturn(true);
            doNothing().when(spy).createEmployee(mockEmployee);
            spy.saveEmployee(mockEmployee);
    
            Mockito.verify(spy).createEmployee(mockEmployee);
        }
    

    注意spy只能使用PowerMockito.doNothing()/doReturn()/doThrow()

    模拟private方法

    现在EmployeeService中添加打印日志的私有方法,saveEmployee方法增加日志打印,在EmployeeServiceTest类添加如下方法:

        public void saveEmployee(Employee employee) {
            if(employee.isNew()) {
                log("new");
                createEmployee(employee);
                return;
            }
            log("update");
            employee.update();
        }
        
        
        private void log(String log) {
            System.out.println(log);
        }
    
        @Test
        public void testPrivateMethod() throws Exception {
            EmployeeService spy = spy(new EmployeeService());
            Employee mockEmployee = mock(Employee.class);
    
            when(mockEmployee.isNew()).thenReturn(true);
            doNothing().when(spy, "log","new");
    
            spy.saveEmployee(mockEmployee);
    
            verifyPrivate(spy).invoke("log","new");
    
        }
    

    小结

    这篇文章介绍了PowerMock的使用,更多详细的使用,请到官方文档查询使用,本文主要内容来源下面这本书,建议仔细阅读实践。

    参考:

    Instant Mock Testing with PowerMock

    相关文章

      网友评论

        本文标题:Android单元测试(五):PowerMock

        本文链接:https://www.haomeiwen.com/subject/hgmqsftx.html