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

一個使用mockito和spring-test的例子

可以在:https://github.com/weipeng2k/mockito-sample找到示例。

       Java單元測試框架在業界非常多,以JUnit爲事實上的標準,而JUnit只是解決了單元測試的基本骨幹,而對於Mock的支持卻沒有。而同樣,在Mock方面,Java也有很多開源的選擇,諸如JMock、EasyMock和Mockito,而Mockito也同樣爲其中的翹楚,二者能夠很好的完成單元測試的工作。本示例就是介紹如何使用二者來完成單元測試。如果公司自己搞一個單元測試框架,維護將成爲一個大問題,而使用業界成熟的解決方案,將會是一個很好的方式。因爲會有一組非常專業的人替你維護,而且不斷地有新的Feature可以使用,同樣你熟悉這些之後你可以不斷的複用這些知識,而不會由於侷限在某個特定的框架下(其實這些特定的框架也只是封裝了業界的開源方案)。使用JUnit做單元測試的主體框架,如果有Spring的支持,可以使用spring-test進行支持,對於層與層之間的Mock,則使用Mockito來完成。

前言

引言

"I'm not a great programmer; I'm just a good programmer with great habits."
-- Kent Beck

       Java單元測試框架在業界非常多,以JUnit爲事實上的標準,而JUnit只是解決了單元測試的基本骨幹,而對於Mock的支持卻沒有。而同樣,在Mock方面,Java也有很多開源的選擇,諸如JMockEasyMockMockito,而Mockito也同樣爲其中的翹楚,二者能夠很好的完成單元測試的工作。本文就是介紹如何使用二者來完成單元測試。

存在的問題

       如果公司自己搞一個單元測試框架,維護將成爲一個大問題,而使用業界成熟的解決方案,將會是一個很好的方式。因爲會有一組非常專業的人替你維護,而且不斷地有新的Feature可以使用,同樣你熟悉這些之後你可以不斷的複用這些知識,而不會由於侷限在某個特定的框架下(其實這些特定的框架也只是封裝了業界的開源方案)。

解決方案

       使用JUnit做單元測試的主體框架,如果有Spring的支持,可以使用spring-test進行支持,對於層與層之間的Mock,則使用Mockito來完成。

使用Mockito進行單元測試

以下例子可以在mockito-test-case中找到。

使用Mockito進行mock

       先看一下怎樣使用Mockito進行一個對象的Mock,首先添加依賴:

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

       接下來嘗試對java.util.List進行Mock,Mock對於List操作的內容進行構造。

構造Mock

       先看一下最簡的使用方式。

public void mock_one() {
    List<String> list = Mockito.mock(List.class);

    Mockito.when(list.get(0)).thenReturn("one");

    System.out.println(list.get(0));

    Assert.assertEquals("one", list.get(0));
}

       上面代碼中Mockito.mock可以構造一個Mock對象,這個對象沒有任何作用,如果調用它的方法,如果有返回值的話,它會返回null。這個時候可以向其中加入mock邏輯,比如:Mockito.when(xxx.somemethod()).thenReturn(xxx),這段邏輯就會在當有外界調用xxx.somemethod()時,返回那個在thenReturn中的對象。

構造一個複雜的Mock

       有時我們需要針對輸入來構造Mock的輸出,簡單的when和thenReturn無法支持,這時就需要較爲複雜的Answer

@Test(expected = RuntimeException.class)
public void mock_answer() {
    List<String> list = Mockito.mock(List.class);
    Mockito.when(list.get(Mockito.anyInt())).thenAnswer(
            invocation -> {
                Object[] args = invocation.getArguments();
                int index = Integer.parseInt(args[0].toString());
                // int index = (int) args[0];
                if (index == 0) {
                    return "0";
                } else if (index == 1) {
                    return "1";
                } else if (index == 2) {
                    throw new RuntimeException();
                } else {
                    return String.valueOf(index);
                }
            });

    Assert.assertEquals("0", list.get(0));
    Assert.assertEquals("1", list.get(1));
    list.get(2);
}

       有時候需要構造複雜的返回邏輯,比如參數爲1的時候,返回一個值,爲2的時候,返回另一個值。那麼when和thenAnswer就可以滿足要求。

       上面代碼可以看到當對於List的任意的輸入Mockito.anyInt(),會進行Answer回調的處理,任何針對List的輸入都會經過它的處理。這可以讓我完成更加柔性和定製化的Mock操作。

斷言選擇

       當然我們可以使用System.out.println來完成目測,但是有時候需要讓JUnit插件或者maven的surefire插件能夠捕獲住測試的失敗,這個時候就需要使用斷言了。我們使用org.junit.Assert來完成斷言的判斷,可以看到通過簡單的assertEquals就可以了,當然該類提供了一系列的assertXxx來完成斷言。

       使用IDEA在進行斷言判斷時非常簡單,比Eclipse要好很多,比如:針對一個int x判斷它等於0,就可以直接寫x == 0,然後代碼提示生成斷言。

真實案例

       下面我們看一個較爲真實的例子,比如:我們有個MemberService用來insertMember。

public interface MemberService {
    /**
     * <pre>
     * 插入一個會員,返回會員的主鍵
     * 如果有重複,則會拋出異常
     * </pre>
     *
     * @param name     name不能超過32個字符,不能爲空
     * @param password password不能全部是數字,長度不能低於6,不超過16
     * @return PK
     */
    Long insertMember(String name, String password) throws IllegalArgumentException;
}

       其對應的實現。

public class MemberServiceImpl implements MemberService {

    private UserDAO userDAO;

    @Override
    public Long insertMember(String name, String password)
            throws IllegalArgumentException {
        if (name == null || password == null) {
            throw new IllegalArgumentException();
        }

        if (name.length() > 32 || password.length() < 6
                || password.length() > 16) {
            throw new IllegalArgumentException();
        }

        boolean pass = false;
        for (Character c : password.toCharArray()) {
            if (!Character.isDigit(c)) {
                pass = true;
                break;
            }
        }
        if (!pass) {
            throw new IllegalArgumentException();
        }

        Member member = userDAO.findMember(name);
        if (member != null) {
            throw new IllegalArgumentException("duplicate member.");
        }

        member = new Member();
        member.setName(name);
        member.setPassword(password);
        Long id = userDAO.insertMember(member);

        return id;
    }

    public void setUserDAO(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

}

       可以看到實現通過聚合了userDAO,來完成操作,而業務層的代碼的單元測試代碼,就必須隔離UserDAO,也就是說要Mock這個UserDAO。

       下面我們就使用Mockito來完成Mock操作。

public class MemberWithoutSpringTest {
    private MemberService memberService = new MemberServiceImpl();

    @Before
    public void mockUserDAO() {
        UserDAO userDAO = Mockito.mock(UserDAO.class);
        Member member = new Member();
        member.setName("weipeng");
        member.setPassword("123456abcd");
        Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);

        Mockito.when(userDAO.insertMember((Member) Mockito.any())).thenReturn(
                System.currentTimeMillis());

        ((MemberServiceImpl) memberService).setUserDAO(userDAO);
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_member_error() {
        memberService.insertMember(null, "123");

        memberService.insertMember(null, null);
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_exist_member() {
        memberService.insertMember("weipeng", "1234abc");
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_illegal_argument() {
        memberService
                .insertMember(
                        "akdjflajsdlfjaasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfasdfsadfasdfasf",
                        "abcdcsfa123");
    }

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

       可以看到,在測試開始的時候,利用了Before來完成Mock對象的構建,也就是說在test執行之前完成了Mock對象的初始化工作。

       但仔細看上述代碼中,MemberService的實現MemberServiceImpl是直接構造出來的,它依賴了實現,但是我們的測試最好不要依賴實現進行測試的。同時UserDAO也是硬塞給MemberService的實現,這是因爲我們常用Spring來裝配類之間的關係,而單元測試沒有Spring的支持,這就使得測試代碼需要硬編碼的方式來進行組裝。

       那麼我們如何避免這樣的強依賴和組裝代碼的出現呢?結論就是使用spring-test來完成。

使用Spring-Test來進行單元測試

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

       spring-test是springframework中一個模塊,主要也是由spring作者Juergen Hoeller來完成的,它可以方便的測試基於spring的代碼。

引入spring-test

       spring-test只需要引入依賴就可以完成測試,非常簡單。它能夠幫助我們啓動一個測試的spring容器,完成屬性的裝配,但是它如何同Mockito集成起來是一個問題,我們採用配置的方式進行。

加入依賴

       增加依賴:

該版本一般和你使用的spring版本一致

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

配置

       由於Mockito支持mock方法構造,所以我們可以將它通過spring factory bean的形式融入到 spring 的體系中。我們針對MemberService進行測試,需要對UserDAO進行Mock,我們只需要在配置中配置即可。

配置在MemberService.xml中,這裏需要說明一下 沒有使用共用的配置文件, 目的就是讓大家在測試的時候能夠相互獨立,而且在一個配置文件中配置的Bean越多,就證明你要測試的類依賴越複雜,也就是越不合理,逼迫自己做重構

<?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-2.5.xsd"
       default-autowire="byName">

    <bean id="memberService" class="com.murdock.tools.mockito.service.MemberServiceImpl"/>

    <bean id="userDAO" class="org.mockito.Mockito" factory-method="mock">
        <constructor-arg>
            <value>com.murdock.tools.mockito.dao.UserDAO</value>
        </constructor-arg>
    </bean>
</beans>

       在進行spring測試之前,我們必須有一個spring的配置文件,用來構造applicationContext,注意上面紅色的部分,這個UserDAO就是MemberServiceImpl需要的,而它利用了spring的FactoryBean方式,通過mock工廠方法完成了Mock對象的構造,其中的構造函數表明了這個Mock是什麼類型的。只用在配置文件中聲明一下就可以了。

構造Mock

       先看一下使用spring-test如何寫單元測試:

@ContextConfiguration(locations = {"classpath:MemberService.xml"})
public class MemberSpringTest extends AbstractJUnit4SpringContextTests {
    @Autowired
    private MemberService memberService;
    @Autowired
    private UserDAO userDAO;

    /**
     * 可以選擇在測試開始的時候來進行mock的邏輯編寫
     */
    @Before
    public void mockUserDAO() {
        Mockito.when(userDAO.insertMember(Mockito.any())).thenReturn(
                System.currentTimeMillis());
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_member_error() {
        memberService.insertMember(null, "123");

        memberService.insertMember(null, null);
    }

    /**
     * 也可以選擇在方法中進行mock
     */
    @Test(expected = IllegalArgumentException.class)
    public void insert_exist_member() {
        Member member = new Member();
        member.setName("weipeng");
        member.setPassword("123456abcd");
        Mockito.when(userDAO.findMember("weipeng")).thenReturn(member);

        memberService.insertMember("weipeng", "1234abc");
    }

    @Test(expected = IllegalArgumentException.class)
    public void insert_illegal_argument() {
        StringBuilder sb = new StringBuilder();
        IntStream.range(0, 32).forEach(sb::append);
        
        memberService.insertMember(sb.toString(), "abcdcsfa123");
    }

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

       可以看到,通過繼承AbstractJUnit4SpringContextTests就可以完成構造applicationContext的功能。當然通過ContextConfiguration指明當前的配置文件所在地,就可以完成applicationContext的初始化,同時利用Autowired完成配置文件中的Bean的獲取。

       由於在MemberService.xml中針對UserDAO的mock配置,對應的mock對象會被注入到MemberSpringTest中,而後續的測試方法就可以針對它來編排mock邏輯。

       我們在Before邏輯中以及方法中均可以自由的裁剪mock邏輯,這樣JUnitspring-testMockito完美的統一到了一起。

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