單元測試之更強大的powermock

前面一篇說到了Mockito的各種功能,可以幫助我們在編寫測試用例的時候模擬對象的各種行爲,但是Mockito對於一些場景還是無法滿足,比方說靜態方法,私有方法(不過一般正常的單元測試很少去mock私有方法),構造方法等

github上關於mockito不支持的地方給瞭如下說明:

Do not mock types you don’t own
Don’t mock value objects
Don’t mock everything
Show some love with your tests

在FAQ中,也寫了mockito的其他一些問題

Mockito 2.x specific limitations

  • Requires Java 6+
  • Cannot mock static methods
  • Cannot mock constructors
  • Cannot mock equals(), hashCode(). Firstly, you should not mock those methods. Secondly, Mockito defines and depends upon a specific implementation of these methods. Redefining them might break Mockito.
  • Mocking is only possible on VMs that are supported by Objenesis. Don’t worry, most VMs should work just fine.
  • Spying on real methods where real implementation references outer Class via OuterClass.this is impossible. Don’t * worry, this is extremely rare case.

Can I mock static methods?
No. Mockito prefers object orientation and dependency injection over static, procedural code that is hard to understand & change. If you deal with scary legacy code you can use JMockit or Powermock to mock static methods.

Can I mock private methods?
No. From the standpoint of testing… private methods don’t exist. More about private methods here.

Can I verify toString()?
No. You can stub it, though. Verification of toString() is not implemented mainly because:

When debugging, IDE calls toString() on objects to print local variables and their content, etc. After debugging, the verification of toString() will most likely fail.
toString() is used for logging or during string concatenation. Those invocations are usually irrelevant but they will change the outcome of verification.

對於上述Mockito不能實現的功能,PowerMock可以滿足我們的需求。PowerMock 也是一個單元測試模擬框架,它是在其它單元測試模擬框架的基礎上做出的擴展。通過提供定製的類加載器以及一些字節碼篡改技巧的應用,PowerMock 實現了對靜態方法、構造方法、私有方法以及 Final 方法的Mock支持等強大的功能。目前,PowerMock 僅支持 EasyMock 和 Mockito。

1.依賴jar包

maven項目需要引入如下jar:

<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.7.4</version>
    <scope>test</scope>
</dependency>
2.mock靜態方法
public class CommonUtils {

    public static String getUUID() {
        String uuid = UUID.randomUUID().toString();
        uuid = uuid.replace("-", "");
        return uuid;
    }
}
public class UUIDTest {

    public String getUUId(){
        return CommonUtils.getUUID();
    }
}

兩個類,一個是靜態工具類,一個是調用工具類的測試類
mock靜態方法的時候就需要使用PowerMockRunner並且添加@PrepareForTest註解,還需要調用對應的mockStatic方法mock靜態方法所在的類

RunWith(PowerMockRunner.class)
@PrepareForTest({CommonUtils.class})
public class CommonUtilsTest {

    @Test
    public void test() throws ParseException {
        PowerMockito.mockStatic(CommonUtils.class);
        PowerMockito.when(CommonUtils.getUUID()).thenReturn("12345678910111111");
        UUIDTest test = new UUIDTest();
        Assert.assertEquals("12345678910111111",test.getUUId());
    }
}
3.mock私有及final方法
public class TargetClass {
    
    public String mockPrivateFunc(int i) {
        return privateFunc(i + "");
    }

    private final String privateFunc(String i) {
        return "0";
    }
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({TargetClass.class})
public class MockTest {

    @Test
    public void testMockPrivateFunc() throws Exception {
        TargetClass targetClass = PowerMockito.spy(new TargetClass());
        PowerMockito.when(targetClass,"privateFunc",anyString()).thenReturn("test");
        String realResult = targetClass.mockPrivateFunc(1);
        Assert.assertEquals("test", realResult);
    }
}

4.mock構造方法
public class User {

    private String username;
    private String password;

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return username+","+password;
    }
}
@RunWith(PowerMockRunner.class)
//注意下這裏添加的是調用構造方法的類而不是User類
@PrepareForTest({UserService.class})
public class UserServiceTest {

    @Test
    public void saveUser() throws Exception {
        String username = "mock姓名";
        String password = "aaa";
        User user = new User(username, password);
        UserService userService = new UserService();
        PowerMockito.whenNew(User.class).withAnyArguments().thenReturn(user);
        userService.saveUser("真實姓名","123");
    }

總結下之裏的規律:
如果需要使用PowerMock來mock構造方法,私有方法,final方法和靜態方法,那麼都需要使用PowerMockRunner和
@PrepareForTest註解

  • 當使用mock構造方法時,註解@PrepareForTest裏寫的類是需要mock的新對象生成的代碼所在的類。
  • 當需要mock系統類的靜態方法的時候,註解裏寫的類是需要調用系統方法所在的類
  • 當需要mock final方法,靜態方法,私有方法的時候,註解@PrepareForTest裏寫的類是對應方法所在的類
5.Field

@InjectMocks和@Mock註解配合使用可以幫我們做自動注入,在編寫單元測試時也可以使用SpringJUnit4ClassRunner來幫助我們做一些屬性的注入和自動裝配。
但是這樣在單機環境下由於spring容器在啓動的時候會自動完成很多初始化工作,一來比較耗時,二來會去連接一些其他中間件比方說配置中心等,單機下就會出現異常

那麼我們就需要PowerMock的field方法來幫助我們做一些裝配的工作

        OperateButtonService operateButtonService = new OperateButtonService();
        Map<String, AbstractGetOperateButton> operateButtonMap = new HashMap<>();
        operateButtonMap.put("CALL_HOTEL", callHotel); 
        operateButtonMap.put("COMMENT", comment);
        operateButtonMap.put("RESERVE_AGAIN", reserveAgain);
        PowerMockito.field(OperateButtonService.class, "operateButtonMap").set(operateButtonService, operateButtonMap);

對operateButtonService的operateButtonMap字段進行賦值,底層原理也是反射,不過powermock幫我們封裝了下,更加容易使用,對於私有變量和靜態變量都可以進行賦值(常量不行)

6.其他註解的功能

@PowerMockRunnerDelegate

如果在使用了PowerMockRunner之後還想使用Spring容器的功能,那麼我們就需要spring的runner

@PowerMockRunnerDelegate(SpringJUnit4ClassRunner.class)

powermock使用了自定義的classloader來解決mock靜態方法與私有方法的問題,因此其會爲加了PrepareForTest註解的類生成對應的classloader來加載用到的類,這樣就可能會導致其與系統的classloader加載了相同的類,導致類型轉換失敗,我們可以使用@PowerMockIgnore註解告訴powermock放棄加載指定的這些類

@PowerMockIgnore({"javax.management.*", "javax.net.ssl.*"})

這也是powermock2.0.0與1.x版本重大不一樣的地方

@SuppressStaticInitializationFor

在單機環境下(無法使用任何外部的資源),因爲代碼中我們可能會在static靜態塊或者常量裏做一些初始化的操作,比方說提前生成Redis操作的管理類等,此時我們就需要避免這些初始化操作,因爲嘗試調用其他服務資源都會失敗

@SuppressStaticInitializationFor(“com.chenpp.RedisManager”)
忽略指定類的靜態初始化, 包括static{}靜態代碼塊和static變量的初始化

PowerMock簡單原理
@RunWith(PowerMockRunner.class)
public class UserServiceTest {

    @Test
    @PrepareForTest({UserService.class})
    public void testSaveUser() throws Exception {
        String username = "mock姓名";
        String password = "aaa";
        User user = new User(username, password);
        Long current = System.currentTimeMillis();
        UserService userService = new UserService();
        PowerMockito.whenNew(User.class).withAnyArguments().thenReturn(user);
        userService.saveUser("真實姓名", "123");
        System.out.println("testSaveUser:" + userService.getClass().getClassLoader());
        System.out.println("testSaveUser:" + user.getClass().getClassLoader());
        System.out.println("testSaveUser:" + System.class.getClassLoader());
    }

    @Test
    public void testUser() throws Exception {
        UserService userService = Mockito.mock(UserService.class, RETURNS_DEEP_STUBS);
        Mockito.when(userService.getUser().show()).thenReturn("mock test");
        Assert.assertEquals("mock test", userService.getUser().show());
        System.out.println("testUser:" + userService.getClass().getClassLoader());
        System.out.println("testUser:" + User.class.getClassLoader());
    }
}

在這裏插入圖片描述
在這裏插入圖片描述

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
    @Test
    public void testUser() throws Exception {
        UserService userService = Mockito.mock(UserService.class, RETURNS_DEEP_STUBS);
        Mockito.when(userService.getUser().show()).thenReturn("mock test");
        Assert.assertEquals("mock test", userService.getUser().show());
        System.out.println("testUser:" + userService.getClass().getClassLoader());
        System.out.println("testUser:" + User.class.getClassLoader());
    }
}

在這裏插入圖片描述
當在某個測試類上使用PowerMockRunner,那麼在運行測試用例時,會創建一個新的org.powermock.core.classloader.MockClassLoader類加載器,然後使用該類加載器加載測試用例使用到的類(系統類除外)

PowerMock會根據你的mock要求,去修改寫在註解@PrepareForTest裏的class文件(當前測試類會自動加入註解中),以滿足特殊的mock需求。例如:去除final方法的final標識,在靜態方法的最前面加入自己的虛擬實現等。

如果需要mock的是系統類的final方法和靜態方法,PowerMock不會直接修改系統類的class文件,而是修改調用系統類的class文件,以滿足mock需求。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章