Spring 測試:其實很簡單

2016-01-30    分類:JAVA開發、程式設計開發、首頁精華1人評論發表於2016-01-30

本文由碼農網 – Sandbox Wang原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃

在過去的職業生涯裡,我經常發現有些人不寫測試程式碼,而他們聲稱不寫的理由是無法輕易地寫出覆蓋多個不同模組的測試用例。好吧,我相信他們中的大部分要麼是缺乏一些比較易掌握的技術手段,要麼就是沒時間來把它搞清楚,畢竟工作中總會有進度之類的各種壓力。因為不知道該如何測試,所以就經常忽略整合測試,由此帶來的問題就是越來越糟糕的軟體、越來越多的BUG和更加失望的客戶。所以我想分享一些個人的經驗,揭開整合測試神祕的面紗。

如何對基於Spring的工程更好地進行整合測試

使用工具: Spring, JUnit, Mockito

想象有這樣一個Spring工程,它整合了一些外部服務,例如,一些銀行的web服務。那麼,為這個工程寫測試用例以及在持續整合系統中完成這些測試時所遇到的問題基本都差不多:

  1. 每次測試都會有交易進行,每次交易都需要付出金錢成本,這些成本最終由客戶承擔;
  2. 測試時發出的過多的請求有可能被認為是惡意請求,可能造成在銀行的賬戶被封,後果是測試失敗;
  3. 當使用非生產環境進行測試時,測試結果並不十分可靠,同樣,後果是測試失敗。

通常情況下,你對單個類進行測試的時候,問題很容易解決,因為你可以虛擬一些外部服務來供呼叫。但是當對整個巨大的業務流程進行測試的時候,意味你需要對多個部件進行測試,這時,需要你將這些部件都納入到Spring容器中進行管理。所幸,Spring包含了非常優秀的測試框架,允許你將來自生產環境配置檔案中的bean注入到測試環境中,但是對那些被呼叫的外部服務,需要我們自己去寫模擬實現。一般人第一反應可能是在測試的setUp階段對由Spring注入的bean進行重新注入(修改),但是這種方法需要再仔細考慮一下。

警告:通過這種方式,你的測試程式碼打破了容器自身的行為,所以沒法保證在真實的環境中也如你測試的結果一樣。

事實上,我們無需先實現模擬類然後再把它重新注入到所需的bean中,我們可以讓Spring幫助我們一開始就注入模擬類。讓我們用程式碼演示一下。

示例工程包含一個名為BankService的類,代表呼叫的外部服務,一個名為UserBalanceService的類,它會呼叫BankService。UserBalanceService實現的非常簡單,僅僅完成將餘額從String向Double型別的轉換。

BankService.java的原始碼:

public interface BankService {
    String getBalanceByEmail(String email);
}

BankServiceImpl.java的原始碼:

public class BankServiceImpl implements BankService {
    @Override
    public String getBalanceByEmail(String email) {
        throw new UnsupportedOperationException("Operation failed due to external exception");
    }
}

UserBalanceService.java的原始碼:

interface UserBalanceService {
    Double getAccountBalance(String email);
}

UserBalanceServiceImpl.java的原始碼:

public class UserBalanceServiceImpl implements UserBalanceService {
    @Autowired
    private BankService bankService;
    @Override
    public Double getAccountBalance(String email) {
        return Double.valueOf(bankService.getBalanceByEmail(email));
    }
}

然後是Spring的XML配置檔案,新增所需要的bean宣告。

applicationContext.xml的原始碼:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd">
    <bean id="bankService" class="ua.eshepelyuk.blog.springtest.springockito.BankServiceImpl"/>
    <bean id="userBalanceService" class="ua.eshepelyuk.blog.springtest.springockito.UserBalanceServiceImpl"/>
</beans>

下面是測試類UserBalanceServiceImplTest.java的原始碼:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/springtest/springockito/applicationContext.xml")
public class UserBalanceServiceImplProfileTest {
    @Autowired
    private UserBalanceService userBalanceService;
    @Autowired
    private BankService bankService;
    @Test
    public void shouldReturnMockedBalance() {
        Double balance = userBalanceService.getAccountBalance("user@bank.com");
        assertEquals(balance, Double.valueOf(123.45D));
    }
}

如我們預料的一樣,測試方法報UnsupportedOperationException異常。我們現在的目的是把BankService換成我們的模擬實現。直接使用Mockito來生成factory bean的方法是沒問題的,但是有更好的選擇,使用Springockito框架。繼續之前可以先大概瞭解一下。

剩下的問題就簡單了:如何讓Spring注入模擬的bean而不是真實的bean,在Spring 3.1版之前除了新建一個XML配置檔案之外沒有其他的方法。但是自從Spring引入了bean的profile定義之後,我們有了更加優雅的解決方式,雖然這種方式也需要一個額外的專門用作測試的XML配置檔案。下面是這個用來測試的配置檔案testApplicationContext.xml的程式碼:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mockito="http://www.mockito.org/spring/mockito"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.mockito.org/spring/mockito https://bitbucket.org/kubek2k/springockito/raw/tip/springockito/src/main/resources/spring/mockito.xsd">
    <import resource="classpath:/springtest/springockito/applicationContext.xml"/>
    <beans profile="springTest">
        <mockito:mock id="bankService" class="ua.eshepelyuk.blog.springtest.springockito.BankService"/>
    </beans>
</beans>

做相應修改過之後的測試類UserBalanceServiceImplProfileTest.java的原始碼:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:/springtest/springockito/testApplicationContext.xml")
@ActiveProfiles(profiles = {"springTest"})
public class UserBalanceServiceImplProfileTest {
    @Autowired
    private UserBalanceService userBalanceService;
    @Autowired
    private BankService bankService;
    @Before
    public void setUp() throws Exception {
        Mockito.when(bankService.getBalanceByEmail("user@bank.com")).thenReturn(String.valueOf(123.45D));
    }
    @Test
    public void shouldReturnMockedBalance() {
        Double balance = userBalanceService.getAccountBalance("user@bank.com");
        assertEquals(balance, Double.valueOf(123.45D));
    }
}

你可能注意到了,在setUp方法裡,我們定義了模擬的行為,並且在類上面加了@Profile的註解。這個註解啟用了名為springTest的profile,因此使用Springockito模擬的bean就可以自動注入到任何它所需要的地方了。這個測試的執行結果會成功,因為Spring注入了Springockito 所模擬的版本,而不是applicationContext.xml裡所宣告的版本。

繼續優化我們的測試

如果我們能將解決這個問題的方法更加推進一步的話,這篇文章看起來才沒有缺憾。Springockito提供了另外一個名字叫作

Springockito Annotation的框架,它允許我們在測試類中使用註解來注入模擬類。繼續看下去之前,您最好先去網站上大概瞧瞧。好了,下面是經過修改後的測試程式碼。

UserBalanceServiceImplAnnotationTest.java的原始碼:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = SpringockitoContextLoader.class,
    locations = "classpath:/springtest/springockito/applicationContext.xml")
public class UserBalanceServiceImplAnnotationTest {
    @Autowired
    private UserBalanceService userBalanceService;
    @Autowired
    @ReplaceWithMock
    private BankService bankService;
    @Before
    public void setUp() throws Exception {
        Mockito.when(bankService.getBalanceByEmail("user@bank.com")).thenReturn(String.valueOf(valueOf(123.45D)));
    }
    @Test
    public void shouldReturnMockedBalance() {
        Double balance = userBalanceService.getAccountBalance("user@bank.com");
        assertEquals(balance, valueOf(123.45D));
    }
}

請注意,這裡並沒有新引入的XML配置檔案,而是直接使用了正式環境的applicationContext.xml。我們使用@ReplaceWithMock這個註解標記了型別為BankService的bean,而後在setUp方法中對模擬類的行為進行了定義。

後記

Springockito-annotations專案有個巨大的優點,那就是,它使我們的測試程式碼建立在依賴覆蓋的基礎之上,通過這樣,我們既不需要定義額外的XML配置檔案,也不需要為了測試而去改動生產環境的配置檔案。如果不使用Springockito-annotations的話,我們除了定義額外的XML配置檔案別無他選了。因此,我強烈建議您在整合測試中使用Springockito-annotations,這樣你可以最大限度減少測試用例對生產程式碼的影響,也能消除維護額外XML配置檔案的負擔。

附言

為Spring工程寫整合測試真是簡單多了吧,文章中的程式碼參考自我的GitHub

譯文連結:http://www.codeceo.com/article/spring-test-is-easy.html
英文原文:Test Me If You Can #1 (Spring Framework)
翻譯作者:碼農網 – Sandbox Wang
轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]

相關文章