Mockito入門:如何在Spring中Mock部分對象

前情提要

隨着分佈式應用的開發逐漸成爲標配,多個微服務團隊合作來完成垂直業務的開發成爲了一種常態。微服務使得團隊可以專注於自己的業務邏輯,在和下游依賴和上游對接的團隊聚焦好接口之後,就進入正式的開發。但是,每個團隊的開發節奏往往不同,下游依賴所提供的服務有些時候不能在自測的時候提供穩定的服務。不僅是多個團隊,單個團隊中每個人所負責的模塊之間也會存在依賴關係,也就同樣存在這樣的問題。

這時候,就需要先在代碼中模擬出依賴的服務,先確保自己開發的代碼中的主流程能夠跑通後。等下游依賴的服務發佈後,再去除模擬的服務,用真實的服務測一遍。

Mock服務可以依賴於一些框架來實現,最經典的就是Mockito。爲什麼最近專門來研究一下Mock對象的方法,是因爲之前爲了Mock下游服務直接修改了源代碼中的實現。舉個例子,本來應該從下游服務中根據用戶ID獲取用戶的詳情信息,包括用戶名,用戶年齡,用戶性別等。但是因爲用戶中心的服務尚未發佈,我直接修改了源代碼中的實現中,返回了一個虛擬的用戶信息。

public Interface UserService{
  UserInfo getUser(String userId);
}

public Class UserServiceImpl implements UserService() {
    @Autowired
    private UserCenter userCenter;
    
    @Override
    public UserInfo getUser(String userId) {
        //註釋了對下游服務的訪問
        //return userCenter.getUser(userId);
        
        //創建了虛擬的用戶信息並返回
        UserInfo userInfo = new UserInfo();
        userInfo.setUserName("xxx");
        ...
        return userInfo;
    }    
}

緊接着,問題來了。在自測完成之後,我忘記了將源代碼中的註釋內容恢復,直接將Mock實現提交到了代碼倉庫中。因爲這個服務不止我一個依賴方調用,導致別人在調用這個接口的時候發現無論怎麼修改用戶ID,獲得的用戶數據都是一樣的。由此,我開始瞭解如何在不修改源代碼的情況下,對服務進行Mock,避免下一次再出現這樣的問題。

Mockito

Mockito是Java單元測試中使用率最高的Mock框架之一。它通過簡明的語法和完整的文檔吸引了大量的開發者。Mockito支持用Maven和Gradle來進行依賴引入和管理。這裏只給出Maven中引入依賴的例子:

        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>mockito-all</artifactId>
            <scope>test</scope>
        </dependency>

下文以JUnit和Mockito兩個框架作爲基礎進行詳細說明

需要測試的Service

依賴的服務1,name方法會返回名稱

public interface ReliedService {

    String name();
}

@Service
public class ReliedServiceImpl implements ReliedService {

    @Override
    public String name() {
        return "rale";
    }
}

依賴的服務2,welcome方法會返回歡迎語

public interface WelcomeLanguageService {

    String welcome();
}

@Service
public class WelcomeLanguageServiceImpl implements WelcomeLanguageService {
    @Override
    public String welcome() {
        return "wow";
    }
}

需要進行測試的服務DemoService。

public interface DemoService {

    String hello();
}

@Service
public class DemoServiceImpl implements DemoService{

    private ReliedService reliedService;

    private WelcomeLanguageService welcomeLanguageService;

    @Override
    public String hello() {
        return welcomeLanguageService.welcome() + " " + reliedService.name();
    }

    //之所以採用setter的方式進行依賴注入,是爲了實現Mock對象的注入
    @Autowired
    public void setReliedService(ReliedService reliedService) {
        this.reliedService = reliedService;
    }

    @Autowired
    public void setWelcomeLanguageService(WelcomeLanguageService welcomeLanguageService) {
        this.welcomeLanguageService = welcomeLanguageService;
    }
}

開啓Mock

方法1. Mockito.mock

直接使用Mockito提供的mock方法即可以模擬出一個服務的實例。再結合when/thenReturn等語法完成方法的模擬實現。

import static org.mockito.Mockito.*;

@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo1 {

    private DemoService demoService;

    @Before
    public void before() {
        demoService = mock(DemoService.class);
    }

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello my friend");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}

方法2. MockitoAnnotations.initMocks(this)

這裏給出了使用@Mock註解來Mock對象時的第一種實現,即使用MockitoAnnotations.initMocks(testClass)。

@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo2 {

    @Mock
    private DemoService demoService;

    @Before
    public void before() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}

方法3. @RunWith(MockitoJUnitRunner.class)(推薦)

在測試用例上帶上了這個註解後,就可以自由的使用@Mock來Mock對象啦。

@RunWith(MockitoJUnitRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo3 {

    @Mock
    private DemoService demoService;

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}

方法4. MockitoRule

這裏需要注意的是如果使用MockitoRule的話,該對象的訪問級別必須爲public。

@RunWith(JUnit4.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class MockDemo4 {

    @Rule
    public MockitoRule rule = MockitoJUnit.rule();

    @Mock
    private DemoService demoService;

    @Test
    public void test() {
        when(demoService.hello()).thenReturn("hello rale");
        System.out.println(demoService.hello());
        verify(demoService).hello();
    }
}

在上面四種方法中,最推薦的就是第二種方法,如果無法使用@RunWith(MockitoJUnitRunner.class)時,再考慮別的兼容的方法。

Stub

標準的Stub在上文中已經給出了簡單的例子,目前Mockito基於BDD(Behavior Driven Development)的思想還提供了類似的given/willReturn的語法。但是,Spring同樣作爲IOC框架,和Mockito的融合存在一定的問題。即如果需要對Spring Bean中的部分依賴進行Stub時,需要手動的去設置。

Mockito其實提供了一個非常方便的註解叫做@InjectMocks,該註解會自動把該單元測試中聲明的Mock對象注入到該Bean中。但是,我在實驗的過程中遇到了問題,即@InjectMocks如果想要標記在接口上,則該接口必須手動初始化,否則會拋出無法初始化接口的異常。但是,如果不使用Spring的自動注入,則必須手動的將該類依賴的別的Bean注入進去。

因此目前使用Mockito的妥協方案是直接@Autowire該接口的實現。然後在上面標記InjectMocks註解,此時會將測試中聲明的Mock對象自動注入,而沒有聲明的依賴的對象依然採用Spring Bean的依賴注入:

@RunWith(MockitoJUnitRunner.class)
@DelegateTo(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = { Application.class })
public class InjectMockTest {

    @Mock
    private WelcomeLanguageService welcomeLanguageService;

    @Autowired
    @InjectMocks
    private DemoServiceImpl demoService;

    @Before
    public void before() {
        given(welcomeLanguageService.welcome()).willReturn("hahaha");
    }
    @Test
    public void test() {
        System.out.println(demoService.hello());
    }
}

DemoService中,WelcomeLanguageService會使用Mock對象,而ReliedService會使用Spring Bean自動注入。

參考文章

Mockito官方文檔

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