天天看點

Mock和Java單元測試中的Mock架構Mockito介紹

什麼是Mock?

    在面向對象程式設計中,模拟對象(英語:mock object,也譯作模仿對象)是以可控的方式模拟真實對象行為的假的對象。程式員通常創造模拟對象(mock object)來測試其他對象的行為,很類似汽車設計者使用碰撞測試假人來模拟車輛碰撞中人的動态行為。

為什麼要使用Mock?

    在單元測試中,模拟對象可以模拟複雜的、真實的(非模拟)對象的行為, 如果真實的對象無法放入單元測試中,使用模拟對象就很有幫助。

在下面的情形,可能需要使用模拟對象來代替真實對象:

真實對象的行為是不确定的(例如,目前的時間或目前的溫度);

真實對象很難搭建起來;

真實對象的行為很難觸發(例如,網絡錯誤);

真實對象速度很慢(例如,一個完整的資料庫,在測試之前可能需要初始化);

真實的對象是使用者界面,或包括使用者界面在内;

真實的對象使用了回調機制;

真實對象可能還不存在;

真實對象可能包含不能用作測試(而不是為實際工作)的資訊和方法。

    例如,一個可能會在特定的時間響鈴的鬧鐘程式可能需要外部世界的目前時間。要測試這一點,測試一直要等到鬧鈴時間才知道鬧鐘程式是否正确地響鈴。如果使用一個模拟對象替代真實的對象,可以變成提供一個鬧鈴時間(不管是否實際時間),這樣就可以隔離地測試鬧鐘程式。

Mockito的簡單使用

    Mockito是GitHub上使用最廣泛的Mock架構,并與JUnit(java單元測試架構)結合使用。Mockito架構可以建立和配置mock對象.使用Mockito簡化了具有外部依賴的類的測試開發!

一般使用Mockito的步驟:

1、模拟任何外部依賴并将這些模拟對象插入測試代碼中

2、執行測試中的代碼

3、驗證代碼是否按照預期執行

    單元測試是每個程式員的一項基本技能,甚至于還出現一種 TDD 的靈活軟體設計開發方法。在我們劃分好子產品進行詳細設計編碼之前,可能隻是粗略的定義了一些接口,在我們進行的前後端分離開發方式實踐中,以及微服務架構的系統設計中,經常會遇到這種情況。

    在我們需要測試的代碼所依賴的服務還未實作,或者說要建構依賴的對象比較困難時,使用Mock的方式進行單元測試是一種比較好的選擇。例如我們在使用 Spring 架構開發和測試 Service 層的代碼時,并不需要等到 Dao 層的相關代碼開發完成才進行單元測試。

    本文主要介紹Java程式設計領域一個非常好用的Mock架構的應用。

    1、Mockito的引入

    Mockito 目前釋出的是 2.x 版本( Mockito 3.x 版本目前還在開發中,會考慮 Java 8 的一些新特性)。我們以 Maven 為例(當然根據自己項目的情況也可以使用 Ivy、Gradle、SBT 等等,甚至直接把 jar 包下載下傳下來放到項目中使用),隻需要在 Maven 項目的 pom 檔案中增加 Mockito 依賴即可。

<dependency>

    <groupId>org.mockito</groupId>

    <artifactId>mockito-core</artifactId>    

    <version>2.23.0</version>

</dependency>

       2、第一段Mock測試代碼。

       考慮一個簡單的使用者注冊功能,我們需要先判斷注冊使用者的使用者名是否被其他使用者注冊過。如果已被注冊過,則注冊失敗,如果未被注冊過,則儲存注冊資訊,注冊成功。下面是代碼設計(示例代碼使用 spring 架構,并使用了 lombok 以減少 POJO 類的 getter,setter 定義):

@Data

public class User {

    private String idUser;      // 使用者ID

    private String username;    // 使用者名

    private String password;    // 使用者密碼

}

public interface UserService {

    boolean regist(User user);

}

@Service

public class UserServiceImpl implements UserService {

    @Autowired

    private UserDao userDao;

    @Override

    public boolean regist(User user) {

        User existUser = userDao.queryByUsername(user.getUsername());

        if (existUser == null) {

            userDao.insertUser(user);

            return true;

        }

        return false;

    }

}

public interface UserDao {

    User queryByUsername(String username);

    void insertUser(User user);

}

    這個時候 UserDao 的實作類還沒有開發,我們要測試 UserService 的 regist 方法時就可以使用 Mock 了。下面就是第一段使用 JUnit 結合 Mockito 編寫的單元測試代碼。

@RunWith(MockitoJUnitRunner.class)

public class UserServiceImplTest {

    String existUsername    = "spiderman";

    String notExistUsername = "ironman";

    @Mock

    private UserDao        userDao;

    @InjectMocks

    private UserServiceImpl userService;

    @Before

    public void setUp() throws Exception {

        // 效果同@RunWith(MockitoJUnitRunner.class)

        // MockitoAnnotations.initMocks(this);

        User existUser = new User();

        existUser.setUsername(existUsername);

        existUser.setPassword("aaaaa");

        // 當調用userDao.queryByUsername入參為"spiderman"時會傳回existUser對象,表示該使用者已存在

        Mockito.when(userDao.queryByUsername(existUsername)).thenReturn(existUser);

        // 當調用userDao.queryByUsername入參為"ironman"時會傳回null值,表示不存在該使用者

        Mockito.when(userDao.queryByUsername(notExistUsername)).thenReturn(null);

    }

    @Test

    public void testExists() throws Exception {

        User testUser = new User();

        testUser.setUsername(existUsername);

        Assert.assertFalse(userService.regist(testUser));

    }

    @Test

    public void testNotExists() throws Exception {

        User testUser = new User();

        testUser.setUsername(notExistUsername);

        Assert.assertTrue(userService.regist(testUser));

    }

}

以上單元測試代碼除了幾行帶有 Mock 字樣的代碼,其他内容和我們之前寫的單元測試沒有差別。從面的代碼我們可以看到 userService 依賴了 userDao ,但是我們的代碼中并沒有執行個體化這兩個對象,并且源代碼中也沒有 UserDao 的具體實作,但是我們的測試代碼卻可以像是已經執行個體化了這兩個對象一樣進行操作。下面我們就來看看這幾行新增代碼的作用。

       3、Mock注解介紹。

       @RunWith(MockitoJUnitRunner.class) 該注解會在test方法執行之前初始化使用 @Spy & @Mock & @InjectMocks 注解的對象;該注解還會自動驗證我們單元測試用Mockito架構的使用情況。如果我們在調用Mockito的靜态方法when之後繼續鍊式調用相應的 stub 方法(如上面示例代碼中去掉thenReturn方法調用),單元測試代碼可以編譯通過,是運作時會報錯。

我們在setup方法中第一行使用MockitoAnnotations.initMocks(this)可以達到該注解同樣的效果。

       當然我們也可以使用 Mockito.mock 方法手動建立 mock 對象,但是并不推薦這樣做。

       @Mock 該注解表示會建立一個mock對象。我們在該 mock 對象上的方法調用并不會實際調用具體的實作代碼(也可能其實本來就還沒有實作)

       @Spy 該注解上面示例代碼并未使用,功能與 @Mock 類似,也是建立一個mock對象,差別在于調用 @Spy 對應的mock對象上的方法時,會實際調用事實實作好的代碼(如果已有實作方法的前提下),但是不會影響我們when-then語句中的定義。但是使用該注解還是會為我們省去對象建立的過程。例如上面示例中如果我們實作了 UserDao 接口:

@Repository

public class UserDaoImpl implements UserDao {

    @Override

    public User queryByUsername(String username) {

        System.out.println("call UserDaoImpl.queryByUsername");

        return null;

    }

    @Override

    public void insertUser(User user) {

    }

}

然後更換單元測試類型的 Mock定義:

@Mock

private UserDao userDao;

更換為

@Spy

private UserDaoImpl userDao;

執行單元測試後我們可以在控制台看到 UserDaoImpl 實作方法中的列印語句:

如果我們再把 @Spy 注解切換回  @Mock 注解,可以發現控制台不會列印 UserDaoImpl 實作方法中的列印語句。

@Spy

privateUserDaoImpl userDao;

更換為

@Mock

privateUserDaoImpl userDao;

@InjectMocks 該注解表示會建立一個測試類的執行個體,并注入依賴的mock對象(@Mock 注解或 @Spy 注解)。

       4、Mock的應用介紹

       除了Mock的注解,下面我們再來看看使用Mock的表達式。上面示例代碼我們展示了when-then表達式的使用。我們使用該語句定義對象方法調用的一些預先約定。

       when方法定義方法場景,指定了具體的mock對象,指定了mock對象的某個具體調用方法,指定了該方法的調用參數值(如果不關心具體的參數值内容,可以用Any代替)。

       then方法定義了我們約定的方法調用之後需要具體執行的操作,比如傳回一個值或者抛出一個異常。

    另外我們還可以使用Mock做一些驗證。例如我們在Assert的斷言方法後面增加一些判斷,如果測試用例是注冊不存在的使用者,我們的業務邏輯中會調用userDao的insertUser方法,這個時候我們可以增加一行Mockito.verify(userDao).insertUser(testUser)。如果我們的 UserServiceImpl 實作中去掉userDao.insertUser(user)調用,測試不會通過,也就提示我們說新注冊的使用者沒有持久化操作需要修複 UserServiceImpl 裡的實作邏輯。

       Mockito在 stackoverflow 是程式員投票最廣泛使用評價最高的一個 Java 程式設計 Mock 架構。使用該架構編寫的單元測試代碼美觀、清潔、易于了解,并且功能強大。本文隻是簡單介紹 Mockito 的一些簡單的基礎知識,一些複雜的進階的功能另文介紹。

作者:金融測試民工

連結:https://www.jianshu.com/p/b8a52260dc22

來源:簡書

著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。