Java单元测试使用PowerMockito打桩Thread.sleep和TimeUnit.xxx.sleep

465 阅读2分钟

背景

在业务代码中,会有需要线程sleep的情况,通常会使用Thread.sleepTimeUnit.xxx.sleep时当前线程暂停执行。
但是在编写单元测试时,如果不对sleep方法进行打桩,会导致单元测试执行时间过长。

举例

当前有一个Add类,在返回结果前sleep 1分钟,如果不对sleep进行打桩,单元测试用例也会执行1分钟。 功能实现类:

public class Add {
    public int add(int a, int b) throws InterruptedException {
        Thread.sleep(60 * 1000);
        return a + b;
    }
}

单元测试类:

class AddTest {
    @Test
    void testAdd() throws InterruptedException {
        Add add = new Add();
        int ret = add.add(1, 2);
        assertEquals(3, ret);
    }
}

执行结果:

image.png 可以看到testAdd这个用例执行了1分钟才结束。

解决方法

使用Thread.sleep的情况

对于使用Thread.sleep的情况,可以通过打桩Thread.class的方式解决。

// 业务逻辑
public class Add {
    public int add(int a, int b) throws InterruptedException {
        Thread.sleep(60 * 1000);
        return a + b;
    }   
}

// 单元测试
@RunWith(PowerMockRunner.class)
@PrepareForTest({Add.class})
class AddTest {
    @Test(expected = InterruptedException.class)
    public void testAdd() throws InterruptedException {
        PowerMockito.mockStatic(Thread.class);
        PowerMockito.doThrow(new InterruptedException()).when(Thread.class);
        Thread.sleep(anyLong());

        Add add = new Add();
        int ret = add.add(1, 2);
        Assert.assertEquals(3, ret);
    }
}

使用TimeUnit.MINUTE.sleep的情况

如果使用TimeUnit.MINUTE.sleep来实现线程sleep,直接打桩Thread.class是无效的。
需要使用下面的方式进行打桩:

// 业务代码
public class Add {
    public int add(int a, int b) throws InterruptedException {
        TimeUnit.MINUTES.sleep(1);
        return a + b;
    }   
}

// 单元测试
@RunWith(PowerMockRunner.class)
@PrepareForTest({Add.class})
class AddTest {
    @Test(expected = InterruptedException.class)
    public void testAdd() throws InterruptedException {
        TimeUnit mockTimeUnit = PowerMockito.mock(TimeUnit.MINUTES.getDeclaringClass());
        Whitebox.setInternalState(TimeUnit.class, "MINUTES", mockTimeUnit);
        PowerMockito.doThrow(new InterruptedException()).when(mockTimeUnit).sleep(anyLong());

        Add add = new Add();
        int ret = add.add(1, 2);
        Assert.assertEquals(3, ret);
    }
}

其他

1. PrepareForTest中是调用sleep方法的类

在本例中,Add这个类调用了Thread和TimeUnit的sleep方法,所以需要将Add.class放入PrepareForTest中。
不需要将Thread.class或者TimeUnit.class放入PrepareForTest中。

2. 除了doThrow外,也可以使用doNothing

除了使用InterruptedException异常来打断线程的sleep外,也可以使线程的sleep正常结束。只需要将上面代码中的doThrow替换为doNothing即可。

两种方法的区别是:

  • 如果调用sleep函数的方法使用了try-catch块并且能够捕获InterruptedException,抛出异常会时方法走进catch代码块。
  • 如果doNothing则不会进入catch代码块。

3. 实际测试发现,上面代码的行为与Java、Mockito的版本有关

上面的代码使用的依赖库的版本如下:

artifactId版本
Jdk1.8
mockito-core2.28.2
powermock-module-junit42.0.9
powermock-api-mockito22.0.9
junit4.13.2

4. Mockito与 PowerMock 版本对应关系

image.png