Android单元测试(五):PowerMock

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容