天天看點

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

一個使用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也有很多開源的選擇,諸如

JMock

EasyMock

Mockito

,而

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是什麼類型的。隻用在配置檔案中聲明一下就可以了。

       先看一下使用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邏輯,這樣

JUnit

spring-test

Mockito

完美的統一到了一起。