如何更好的做單元測試並用它來提升程式碼質量(下)

weipeng2k發表於2018-09-25

現代化的spring-test使用方式

以下例子可以在javaconfig-spring-test中找到。

       在classic-spring-test中演示的單元測試,還是用配置檔案的方式,但是從Spring4之後,官方就鼓勵使用Java的方式對spring進行配置,而不是用以前那樣的xml配置形式了,因此我們基於註解可以來簡化單元測試的編寫,我們稱之為現代化的spring-test方式。

修改單元測試

       測試不用繼承AbstractJUnit4SpringContextTests,通過註解即可,然後對於bean的配置,可以通過Java配置風格完成

註解

       使用RunWithContextConfiguration配置即可將一個類宣告為支援Spring容器的測試用例。

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {
}
註解 說明
RunWith 該註解是junit提供的,表示用那種方式來執行這個測試,這裡是SpringRunner,由spring-test提供
ContextConfiguration 對測試的Spring容器的配置,比如:配置的位置等

配置與示例

       通過註解可以宣告按照何種方式去執行測試,以及測試的Spring容器如何組裝,但是還或缺在Spring容器中如何配置Bean,以前這是通過xml來進行配置的。

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = MemberJavaConfigTest.MemberServiceConfig.class)
public class MemberJavaConfigTest {

    @Autowired
    private MemberService memberService;

    @Test
    public void insert_member() {
        System.out.println(memberService.insertMember("windowsxp", "abc123"));
        Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
    }

    @Configuration
    static class MemberServiceConfig {

        @Bean
        public MemberService memberService(UserDAO userDAO) {
            MemberServiceImpl memberService =  new MemberServiceImpl();
            memberService.setUserDAO(userDAO);
            return memberService;
        }

        @Bean
        public UserDAO userDAO() {
            UserDAO mock = Mockito.mock(UserDAO.class);
            Mockito.when(mock.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
            return mock;
        }
    }
}

       可以看到只需要有一個類,被註解了Configuration,該類就是一個配置型別,而這種Java Config Style已經是Spring官方推薦的方式了。

       Bean註解類似xml中的bean標籤,這裡配置了兩個Bean一個MemberService的實現,另外一個是mock的UserDAO。其中對MemberService的配置需要依賴UserDAO

       剩下的測試過程就和之前classic-spring-test完全一致了,可以看到新的方式沒有了惱人的xml配置,變得更加直接和高效。

SpringBoot環境下的測試方法

以下例子可以在spring-boot-test中找到。

       Spring框架實際上是依靠SpringBoot完成了續命,由它煥發了第二春,開啟了一個全新的戰場。在今天微服務大放異彩的環境下,針對SpringBoot的測試也會有所不同。

       SpringBoot實際是用來啟動你的應用,所以它會有配置以及一系列約定大於配置的環境準備,所以需要依賴spring-boot-test支援來完成單元測試。

修改單元測試

       如果需要在單元測試啟動時啟動SpringBoot,需要做一下相關的配置,增加一些註解。

依賴

       增加依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-test</artifactId>
    <scope>test</scope>
</dependency>

註解

       和JavaConfig的方式非常類似,通過註解可以宣告該測試是SpringBootTest,並且可以指定執行的SpringBoot容器的配置。

@SpringBootTest(classes = SpringBootMemberTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@RunWith(SpringRunner.class)
public class SpringBootMemberTest {
註解 說明
SpringBootTest 描述了該SpringBoot單元測試是根據哪個配合來啟動容器
TestPropertySource 應用的配置使用哪個

配置

       通過註解可以宣告按照何種方式去執行測試,以及測試的Spring容器如何組裝,還或缺在Spring容器中如何配置Bean。

@SpringBootTest(classes = SpringBootMemberTest.Config.class)
@TestPropertySource(locations = "classpath:test-application.properties")
@RunWith(SpringRunner.class)
public class SpringBootMemberTest {

    @Autowired
    private Environment env;
    @MockBean
    private UserDAO userDAO;
    @Autowired
    private MemberService memberService;

    @Test
    public void environment() {
        Assert.assertEquals("Alibaba", env.getProperty("brand-owner.name"));
    }

    @Before
    public void init() {
        Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(System.currentTimeMillis());
    }

    @Test
    public void insert_member() {
        System.out.println(memberService.insertMember("windowsxp", "abc123"));
        Assert.assertNotNull(memberService.insertMember("windowsxp", "abc123"));
    }

    @Configuration
    static class Config {

        @Bean
        public MemberService memberService(UserDAO userDAO) {
            MemberServiceImpl memberService =  new MemberServiceImpl();
            memberService.setUserDAO(userDAO);
            return memberService;
        }
    }

}

       可以看到新增了一個註解MockBean,這個用來幫助我們建立一個Mock的UserDAO,而不用通過編碼來進行建立,回憶之前在classic以及javaconfig中的Mock方式,都需要呼叫Mockito.mock(Class type)方法來建立一個Mock物件,而在SpringBootTest中就不需要了,直接在成員變數上增加MockBean的註解就可以了。

       同時可以看到在單元測試中增加了一個注入屬性,Environment,它代表Spring執行的環境,可以從中獲取配置,以下是test-application.application中的內容:

brand-owner.name=Alibaba
brand-owner.company=Alibaba-inc.

       在environment測試方法中,可以訪問測試的配置內容,從這裡可以看到SpringBootTestspring-test基礎上,除了啟動一個Spring容器,還準備好了一個SpringBoot執行時環境。

       但是從側面上講,使用SpringBootTest就依賴了執行時環境,這不是一個好的選擇,所以在大多數情況下,對於程式碼的單元測試spring-test就可以完全應對。

單元測試覆蓋率

       就像刻意的刷分數一樣,單元測試覆蓋率也是一個我們追求的目標,當單元測試行覆蓋率超過70%的時候,整個專案的質量會很不錯。持續穩定的單元測試覆蓋率,會保障一個應用一直處於較穩定的狀態,後續投入維護的資源會降低。

       在不少IDE中,如:IDEA,都內建了統計單元測試的工具,只需要按照package執行測試即可,在這裡我們不依賴具體的IDE,而是用maven外掛來做。

jacoco

       該外掛對java8的語法支援較好,在pom檔案中增加配置。

<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.1</version>
    <executions>
        <execution>
            <id>prepare-agent</id>
            <goals>
                <goal>prepare-agent</goal>
            </goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>prepare-package</phase>
            <goals>
                <goal>report</goal>
            </goals>
        </execution>
        <execution>
            <id>post-unit-test</id>
            <phase>test</phase>
            <goals>
                <goal>report</goal>
            </goals>
            <configuration>
                <dataFile>target/jacoco.exec</dataFile>
                <outputDirectory>target/jacoco-ut</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

       當執行mvn test時會生產單元測試覆蓋率報告。

位置一般在專案的 target/jacoco-ut 目錄下。

覆蓋率

       開啟目錄下的index.html可以看到各個類的覆蓋率情況。

<img src="https://raw.githubusercontent.com/weipeng2k/mockito-sample/master/resources/chapter6-1.png" />

缺失路徑

       點選到對應的package中的類,可以檢視缺失的測試路徑,這樣就可以指導哪些分支沒有納入單測。

<img src="https://raw.githubusercontent.com/weipeng2k/mockito-sample/master/resources/chapter6-2.png" />

測試驅動開發簡介

       測試驅動開發的基本思想就是在開發功能程式碼之前,先編寫測試程式碼,然後只編寫使測試通過的功能程式碼,從而以測試來驅動整個開發過程的進行。這有助於編寫簡潔可用和高質量的程式碼,有很高的靈活性和健壯性,能快速響應變化,並加速開發過程。

測試開發驅動模式

       測試驅動開發的基本過程如下:

  • 快速新增一個測試
  • 執行所有的測試(有時候只需要執行一個或一部分),發現新增的測試不能通過
  • 做一些小小的改動,儘快地讓測試程式可執行,為此可以在程式中使用一些不合情理的方法
  • 執行所有的測試,並且全部通過
  • 重構程式碼,以消除重複設計,優化設計結構

       簡單來說,就是不可執行/可執行/重構——這正是測試驅動開發的口號。

可取之處

       測試驅動開發能夠讓程式碼上生產環境之前,能夠以使用者的角度審視編寫的程式碼:

  • 如果程式碼難測,那就是對問題的分析還沒有到位
  • 如果大量的Mock,那就是依賴過於複雜

       除了能夠通過反向刺激讓我們看到程式碼的不足,它還能以使用者的角度去看:

  • 這個方法命名是否夠妥帖
  • 別人用這個函式會誤用嗎
  • 這個類是不是承擔了過多的職責


相關文章