Spring MVC -- 單元測試和集成測試

測試在軟件開發中的重要性不言而喻。測試的主要目的是儘早發現錯誤,最好是在代碼開發的同時。邏輯上認爲,錯誤發現的越早,修復的成本越低。若是在編程中發現錯誤,能夠當即更改代碼;若是軟件發佈後,客戶發現錯誤所須要的修復成本會很大。html

在軟件開發中有許多不一樣的測試,其中兩個是單元測試和集成測試。一般從單元測試開始測試類中的單個方法,而後進行集成測試,以測試不一樣的模塊是否能夠無縫協同工做。java

本篇博客中的示例使用JUnit測試框架以及Spring test模塊。Spring test模塊中的API可用於單元測試和集成測試。能夠在org.springframework.test及其子包以及org.springframework.mock.*包中找到Spring測試相關的類型。ios

一 單元測試

單元測試的理想狀況下是爲每一個類建立一個測試類,併爲類中的每一個方法建立一個測試方法,像getter和setter這樣的簡單方法除外,他們直接從字段返回值或賦值給字段。git

在測試語中,被測試的類稱爲被測系統(SUT)。程序員

單元測試旨在快速且屢次運行。單元測試僅驗證代碼自己,而不涉及它的依賴,其任何依賴應該被幫助對象代替。設計依賴項的測試一般在集成測試中完成,而不是在單元測試。github

你可能會問,咱們可使用main()方法從類自己內測試一個類,爲何還須要單元測試內?這主要是由於,單元測試具備如下好處:web

  • 在單獨測試類在編寫測試代碼不會混淆你的類;
  • 單元測試能夠用於迴歸測試,在一些邏輯發生變化時,以確保一切仍然工做;
  • 單元測試能夠在持續集成設置中自動化測試;持續集成是指一種開發方法,當程序員將他們的代碼提交到共享庫時,每次代碼提交將觸發一次自動構建並運行全部單元測試,持續集成能夠儘早的檢測問題。

在單元測試中,類使用new運算符實例化。不依賴Spring框架的依賴注入容易來建立bean。spring

下面咱們建立一個被測試類MyUtility:數據庫

package com.example.util;
public class MyUtility{
    public int method1(int a,int b){...}
    public long method(long a){...}  
}

爲了對這個類進行測試,建立一個MyUtilityTest類,注意每一個方法應該至少有一個測試方法:express

package com.example.util;
public class MyUtilityTest{
    public void testMethod1(){
        MyUtility utility = new MyUtility();
        int result = utility.method1(100,200);
        //assert that result equals the expected value
    }
     public void testMethod2(){
        MyUtility utility = new MyUtility();
        long result = utility.method2(100L);
        //assert that result equals the expected value
    }
}

單元測試有些約定俗成,首先是將測試類命名爲與帶有Test的SUT相同的名稱。所以,MyUtility的測試類應命名爲MyUtilityTest;其次,測試類的包路徑應與SUT相同,以容許前者訪問後者的公開和默認成員。可是,測試類應位於不一樣於測試的類的源文件夾下。

測試方法沒有返回值。在測試方法中,你實例化要測試的類,調用要測試的方法並驗證結果。爲了使測試類更容易編寫,你應該使用測試框架,例如JUnit或TestNG。

這一節將會介紹用JUnit編寫測試類的例子,JUnit事實上是Java的標準單元測試框架。

一、應用JUnit

對於單元測試,推薦使用JUnit。咱們能夠從http://junit.org下載它。咱們須要下載junit.jar和org.hamcrest.core.jar文件,後者是JUnit的依賴項,目前junit.jar版本爲4.12。

若是你使用的是Maven或STS,請將元素添加到pom.xml文件如下載JUnit及其依賴關係:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>

若是想了解更多JUnit的信息和使用,能夠閱讀官方文檔。

二、開發一個單元測試

編寫單元測試很簡單,使用@Test簡單的註解全部測試方法。此外,能夠經過@Before註解來建立初始化方法,初始化方法在調用任何測試方法以前調用。咱們還能夠經過@After註解方法來建立清理方法,清理方法在測試類中的全部測試方法執行以後調用,而且能夠來釋放測試期間使用的資源。

下面展現了須要進行單元測試的Calculator類:

package com.example;

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
    
    public int subtract(int a, int b) {
        return a - b;
    }
}

而後咱們建立一個單元測試CalculatorTest類:

package com.example;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

public class CalculatorTest {

    @Before
    public void init() {
    }
    
    @After
    public void cleanUp() {
    }
    
    @Test
    public void testAdd() {
        Calculator calculator = new Calculator();
        int result = calculator.add(5, 8);
        Assert.assertEquals(13, result);
    }
    
    @Test
    public void testSubtract() {
        Calculator calculator = new Calculator();
        int result = calculator.subtract(5, 8);
        Assert.assertEquals(-3, result);
    }
    
}

CalculatorTest類有兩個方法,一個初始化方法和一個清除方法。org.junit.Assert類提供用於聲明結果的靜態方法。例如,assertEquals()方法用來能比較兩個值。

三、運行一個單元測試 

Eclipse知道一個類是不是一個JUnit測試類。要運行測試類,請右鍵單擊包資源管理器中的測試類,而後選擇運行方式Run As JUnit Test。

測試完成後,若是JUnit視圖還沒有打開,Eclipse將打開它。若是單元測試成功完成後,將會在JUnit視圖中看到一個綠色條:

四、經過測試套件來運行

在有十幾個類的小項目中,你將有十幾個測試類。在一個更大的項目中,你會有更多測試類。在Eclipse中,運行一個測試類很容易,可是如何運行全部的測試類。

使用JUnit的解決方案很是簡單。建立一個Java類並使用@RunWith(Suite.class)和@SuiteClasses()註解它。後者應該列出你想要運行的全部類和其餘套件測試。

下面演示一個測試套件:

package com.example;

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;

@RunWith(Suite.class)
@SuiteClasses({ CalculatorTest.class, MathUtilTest.class })
public class MyTestSuite {

}

二 應用測試擋板(Test Doubles)

被測系統(SUT)不多孤立存在。一般爲了測試一個類,你須要依賴。在測試中,你的SUT所須要的依賴稱爲協做者。

協做者常常被稱爲測試擋板的其餘對象取代。使用測試擋板有幾個緣由:

  • 在編寫測試類時,真正的依賴尚未準備好;
  • 一些依賴項,例如HttpServletRequest和HttpServletResponse對象,是從servlet容器獲取的,而本身建立這些對象將會很是耗時;
  • 一些依賴關係啓動和初始化速度較慢。例如,DAO對象訪問數據庫致使單元測試執行很慢;

測試擋板在單元測試中普遍使用,也用於集成測試。當前有許多用於建立測試擋板的框架,Spring也有本身的類來建立測試擋板。

模擬框架可用於建立測試模板和驗證代碼行爲,這裏有一些流行的框架:

  • Mockito;
  • EasyMock;
  • jMock

除了上面的庫,Spring還附帶了建立模擬對象的類。不過,這一節,只詳細介紹如何使用Mockito。

使用Mockito須要Mockito的發佈包(一個mockito.jar文件)及其依賴(一個objenesis.jar文件),這裏給出一個整合後的jar包下載網址:http://maven.outofmemory.cn/org.mockito/mockito-all/

在開始寫測試擋板以前,須要先學習理論知識。以下是測試擋板的5種類型:

  • dummy;
  • stub;
  • spy;
  • fake;
  • mock;

這些類型中的每一種將在下面的小節中解釋。

一、dummy

dummy是最基本的測試擋板類型。一個dummy是一個協做者的實現,它不作任何事情,並不改變SUT的行爲。它一般用於使SUT能夠實例化。dummy只是在開發的早起階段使用。

例如,建立一個ProductServiceImpl類,這個類依賴於傳遞給構造函數的ProductDAO:

package com.example.service;
import java.math.BigDecimal;

import com.example.dao.ProductDAO;

public class ProductServiceImpl implements ProductService {

    private ProductDAO productDAO;

    public ProductServiceImpl(ProductDAO productDAOArg) {
        if (productDAOArg == null) {
            throw new NullPointerException("ProductDAO cannot be null.");
        }
        this.productDAO = productDAOArg; 
    }

    @Override
    public BigDecimal calculateDiscount() {
        return productDAO.calculateDiscount();
    }
    
    @Override
    public boolean isOnSale(int productId) {
        return productDAO.isOnSale(productId);
    }
}

ProductServiceImpl類須要一個非空的ProductDAO對象來實例化。同時,要測試的方法不使用ProductDAO。所以,能夠建立一個dummy對象,只需讓ProductServiceImpl可實例化:

package com.example.dummy;

import java.math.BigDecimal;

import com.example.dao.ProductDAO;

public class ProductDAODummy implements ProductDAO {
    public BigDecimal calculateDiscount() {
        return null;
    }
    public boolean isOnSale(int productId) {
        return false;
    };
}

在dummy類中的方法實現什麼也不作,它的返回值也不重要,由於這些方法從未使用過。

下面顯示一個能夠運行的測試類ProductServiceImplTest:

package com.example.dummy;

import static org.junit.Assert.assertNotNull;

import org.junit.Test;

import com.example.dao.ProductDAO;
import com.example.service.ProductService;
import com.example.service.ProductServiceImpl;

public class ProductServiceImplTest {

    @Test
    public void testCalculateDiscount() {
        ProductDAO productDAO = new ProductDAODummy();
        ProductService productService = new ProductServiceImpl(productDAO);
        assertNotNull(productService);
    }

}

ProductService接口:

package com.example.service;

import java.math.BigDecimal;

public interface ProductService {
    BigDecimal calculateDiscount();
    boolean isOnSale(int productId);

}
View Code

ProductDAO接口:

package com.example.dao;

import java.math.BigDecimal;

public interface ProductDAO {
    BigDecimal calculateDiscount();
    boolean isOnSale(int productId);
}
View Code

二、stub

像dummy同樣,stub也是依賴接口的實現。和dummy 不一樣的是,stub中的方法返回硬編碼值,而且這些方法被實際調用。

下面建立一個stub,能夠用於測試ProductServiceImpl類:

package com.example.stub;

import java.math.BigDecimal;

import com.example.dao.ProductDAO;

public class ProductDAOStub implements ProductDAO {
    public BigDecimal calculateDiscount() {
        return new BigDecimal(14);
    }
    public boolean isOnSale(int productId) {
        return false;
    };
}

下面顯示一個能夠運行的測試類ProductServiceImplTest:

package com.example.stub;

import static org.junit.Assert.assertNotNull;

import org.junit.Test;

import com.example.dao.ProductDAO;
import com.example.service.ProductService;
import com.example.service.ProductServiceImpl;

public class ProductServiceImplTest {

    @Test
    public void testCalculateDiscount() {
        ProductDAO productDAO = new ProductDAOStub();
        ProductService productService = new ProductServiceImpl(productDAO);
        assertNotNull(productService);
    }

}

三、spy

spy是一個略微智能一些的sub,由於spy能夠保留狀態。考慮下面的汽車租賃應用程序。其中包含一個GarageService接口和一個GarageServiceImpl類。

GarageService接口:

package com.example.service;

import com.example.MyUtility;

public interface GarageService {
    MyUtility rent();
}

GarageServiceImpl類:

package com.example.service;

import com.example.MyUtility;
import com.example.dao.GarageDAO;

public class GarageServiceImpl implements GarageService {
    private GarageDAO garageDAO;
    public GarageServiceImpl(GarageDAO garageDAOArg) {
        this.garageDAO = garageDAOArg;
    }
    public MyUtility rent() {
        return garageDAO.rent();
    }
}

GarageService接口只有一個方法:rent()。GarageServiceImpl類是GarageService的一個實現,而且依賴一個GarageDAO ,GarageServiceImpl中的rent()方法調用GarageDAO 中的rent()方法。

package com.example.dao;

import com.example.MyUtility;

public interface GarageDAO {
    MyUtility rent();
}

GarageDAO 的實現rent()方法應該返回一個汽車,若是還有汽車在車庫:或者返回null,若是沒有更多的汽車。

因爲GarageDAO的真正實現尚未完成。建立一個GarageDAOSpy類被用做測試擋板,它是一個spy,由於它的方法返回一個硬編碼值,而且它經過一個carCount變量來確保車庫裏的車數。

package com.example.spy;

import com.example.MyUtility;
import com.example.dao.GarageDAO;

public class GarageDAOSpy implements GarageDAO {
    private int carCount = 3;
    
    @Override
    public MyUtility rent() {
        if (carCount == 0) {
            return null;
        } else {
            carCount--;
            return new MyUtility();
        }   
    }
}

下面顯示了使用GarageDAOSpy測試GarageServiceImplTest類的一個測試類:

package com.example.spy;

import org.junit.Test;

import com.example.MyUtility;
import com.example.dao.GarageDAO;
import com.example.service.GarageService;
import com.example.service.GarageServiceImpl;

import static org.junit.Assert.*;

public class GarageServiceImplTest {

    @Test
    public void testRentCar() {
        GarageDAO garageDAO = new GarageDAOSpy();
        GarageService garageService = new GarageServiceImpl(garageDAO);
        MyUtility car1 = garageService.rent();
        MyUtility car2 = garageService.rent();
        MyUtility car3 = garageService.rent();
        MyUtility car4 = garageService.rent();
        
        assertNotNull(car1);
        assertNotNull(car2);
        assertNotNull(car3);
        assertNull(car4);
    }

}

因爲在車庫中只有3輛車,spy智能返回3輛車,當第四次調用其rent()方法時,返回null。

四、fake

fake的行爲就像一個真正的協做者,但不適合生成,由於它走「捷徑」。內存存儲是一個fake的完美示例,由於它的行爲像一個DAO,不會將其狀態保存到硬盤驅動器。

咱們建立一個Member實體類:

package com.example.model;

public class Member {
    private int id;
    private String name;
    public Member(int idArg, String nameArg) {
        this.id = idArg;
        this.name = nameArg;
    }

    public int getId() {
        return id;
    }
    public void setId(int idArg) {
        this.id = idArg;
    }

    public String getName() {
        return name;
    }
    public void setName(String nameArg) {
        this.name = nameArg;
    }
}

而後建立一個MemberServiceImpl類,其實現了MemberService接口:

package com.example.service;

import java.util.List;

import com.example.model.Member;

public interface MemberService {
    public void add(Member member);

    public List<Member> getMembers();

}

MemberServiceImpl類能夠將Member對象成員添加到memberDAO並檢索全部存儲的成員:

package com.example.service;

import java.util.List;

import com.example.dao.MemberDAO;
import com.example.model.Member;

public class MemberServiceImpl implements MemberService {

    private MemberDAO memberDAO;

    public void setMemberDAO(MemberDAO memberDAOArg) {
        this.memberDAO = memberDAOArg;
    }

    @Override
    public void add(Member member) {
        memberDAO.add(member);
    }

    @Override
    public List<Member> getMembers() {
        return memberDAO.getMembers();
    }

}

MemberServiceImpl依賴於MemberDAO。可是,因爲沒有可用的MemberDAO實現,咱們能夠建立一個MemberDAO的fake實現MemberDAOFake類,以即可以當即測試MemberServiceImpl。MemberDAOFake類它將成員存儲在ArrayList中,而不是持久化存儲。所以,不能在生成中使用它,可是對於單元測試是足夠的:

package com.example.fake;
import java.util.ArrayList;
import java.util.List;

import com.example.dao.MemberDAO;
import com.example.model.Member;

public class MemberDAOFake implements MemberDAO {
    private List<Member> members = new ArrayList<>();
        
    @Override
    public void add(Member member) {
        members.add(member);
    }

    @Override
    public List<Member> getMembers() {
        return members;
    }
}

下面咱們將會展現一個測試類MemberServiceImplTest ,它使用MemberDAOFake做爲MemberDAO的測試擋板來測試MemberServiceImpl類:

package com.example.service;

import org.junit.Assert;
import org.junit.Test;

import com.example.dao.MemberDAO;
import com.example.fake.MemberDAOFake;
import com.example.model.Member;

public class MemberServiceImplTest {

    @Test
    public void testAddMember() {
        MemberDAO memberDAO = new MemberDAOFake();
        MemberServiceImpl memberService = new MemberServiceImpl();   
        memberService.setMemberDAO(memberDAO);
        memberService.add(new Member(1, "John Diet"));
        memberService.add(new Member(2, "Jane Biteman"));
        Assert.assertEquals(2, memberService.getMembers().size());
    }
}

五、mock

mock導入理念上不一樣於其它測試擋板。使用dummy、stub、spy和fake來進行狀態測試,即驗證方法的輸出。而使用mock來執行行爲(交互)測試,以確保某個方法真正被調用,或者驗證一個方法在執行另外一個方法期間被調用了必定的次數。

建立一個MathUtil類:

package com.example;
public class MathUtil {
    private MathHelper mathHelper;
    public MathUtil(MathHelper mathHelper) {
        this.mathHelper = mathHelper;
    }
    public MathUtil() {
        
    }
    
    public int multiply(int a, int b) {
        int result = 0;
        for (int i = 1; i <= a; i++) {
            result = mathHelper.add(result, b);
        }
        return result;
    }
    
}

MathUtil類有一個方法multiply(),它很是直接,使用多個add()方法類。換句話說,3x8計算爲8+8+8。MathUtil類並不知道如何執行add()。由於,它依賴於MathHelper對象:

package com.example;

public class MathHelper {
    public int add(int a, int b) {
        return a + b;
    }
}

測試所關心的並非multiply()方法的結果,而是找出方法是否如預期同樣執行。所以,在計算3x8時,它應該調用MathHelper對象add()方法3次。

下面咱們展現了一個使用MathHelper模擬的測試類。Mockito是一個流行的模擬框架,用於建立模擬對象。

package com.example;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import org.junit.Test;

public class MathUtilTest {
    
    @Test
    public void testMultiply() {
        MathHelper mathHelper = mock(MathHelper.class);
        for (int i = 0; i < 10; i++) {
            when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8);
        }
        MathUtil mathUtil = new MathUtil(mathHelper);
        mathUtil.multiply(3, 8);
        verify(mathHelper, times(1)).add(0, 8);
        verify(mathHelper, times(1)).add(8, 8);
        verify(mathHelper, times(1)).add(16, 8);
    }
}

使用Mockito建立mock對象很是簡單,只需調用org.mockito.Mockito的靜態方法mock(),下面展現如何建立MathHelper  mock對象:

MathHelper mathHelper = mock(MathHelper.class);

解下來,你須要使用when()方法準備mock對象。基本上,你告訴它,給定使用這組參數的方法調用,mock對象必須返回這個值。例如,這條語句是說若是調用mathHelper.add(10,20),返回值必須是10+20:

when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8);

對於此測試,準備具備十組參數的mock對象(但不是全部的參數都會被使用)。

for (int i = 0; i < 10; i++) {
     when(mathHelper.add(i * 8, 8)).thenReturn(i * 8 + 8);
}

而後建立要測試的對象並調用其參數:

MathUtil mathUtil = new MathUtil(mathHelper);
mathUtil.multiply(3, 8);

接下來的3條語句是行爲測試。爲此,調用verify()方法:

verify(mathHelper, times(1)).add(0, 8);
verify(mathHelper, times(1)).add(8, 8);
verify(mathHelper, times(1)).add(16, 8);

第一條語句驗證mathHelper.add(0,8)被調用了一次,第二條語句驗證mathHelper.add(8,8)被調用了一次,第三條語句驗證mathHelper.add(16,8)被調用了一次。

三 對Spring MVC Controller單元測試

在前幾節中已經介紹瞭如何在Spring MVC應用程序中測試各個類。可是Controller有點不一樣,由於它們一般與Servlet API對象(如HttpServletRequest、HttpServletResponse、HttpSession等)交互。在許多狀況下,你將須要模擬這些對象以正確測試控制器。

像Mockito或EasyMock這樣的框架是能夠模擬任何Java對象的通用模擬框架,可是你必須本身配置生成的對象(使用一系列的when語句)。而Spring Test模擬對象是專門爲使用Spring而構建的,而且與真實對象更接近,更容易使用,如下討論其中一些重要的單元測試控制器類型。

一、MockHttpServletRequest和MockHttpServletResponse

當調用控制器時,你可能須要傳遞HttpServletRequest和HttpServletResponse。在生產環境中,兩個對象都由servlet容器自己提供。在測試環境中,你可使用org.springframework.mock.web包中的MockHttpServletRequest和MockHttpServletResponse類。

這兩個類很容易使用。你能夠經過調用其無參構造函數來建立實例:

 MockHttpServletRequest request = new MockHttpServletRequest();
 MockHttpServletResponse response = new MockHttpServletResponse();

MockHttpServletRequest類實現了javax.servlet.http.HttpServletRequest,並容許你將實例配置看起來像一個真正的HttpServletRequest。它提供了方法來設設置HttpServletRequest中的全部屬性以及獲取器屬性的值,下表顯示了它的一些方法

方法 描述
addHeader 添加一個HTTP請求頭
addParameter 添加一個請求參數
getAttribute 返回一個屬性
getAttributeNames 返回包含了所有屬性名的一個Enumeration對象
getContextPath 返回上下文路徑
getCookies 返回所有的cookies
setMethod 設置HTTP方法
setParameter 設置一個參數值
setQueryString 設置查詢語句
setRequestURI 設置請求URI

MockHttpServletResponse類實現了javax.servlet.http.HttpServletResponse,並提供了配置實例的其它方法,下表顯示了其中一些主要的方法:

方法 描述
addCookie 添加一個cookie
addHeader 添加一個HTTP請求頭
getContentLength 返回內容長度
getWriter 返回Writer
getOutputStream 返回ServletOutputStream

下面演示一個例子。首先建立一個控制器類VideoController:

package com.example.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class VideoController {
    @RequestMapping(value = "/mostViewed")
    public String getMostViewed(HttpServletRequest request, HttpServletResponse response) {
        Integer id = (Integer) request.getAttribute("id");
        if (id == null) {
            response.setStatus(500);
        } else if (id == 1) {
            request.setAttribute("viewed", 100);
        } else if (id == 2) {
            request.setAttribute("viewed", 200);
        }
        return "mostViewed";
    }
}

VideoController類的getMostViewed()方法中,若請求屬性id存在且值爲1或2,則添加請求屬性「viewed」。不然,不添加請求屬性。

咱們建立一個測試類VideoControllerTest,使用兩個測試方法來驗證VideoController:

package com.example.controller;

import org.junit.Test;
import static org.junit.Assert.*;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;

public class VideoControllerTest {
    @Test
    public void testGetMostViewed() {
        VideoController videoController = new VideoController();
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setRequestURI("/mostViewed");
        request.setAttribute("id", 1);
        MockHttpServletResponse response = new MockHttpServletResponse();

        // must invoke
        videoController.getMostViewed(request, response);
        
        assertEquals(200, response.getStatus());
        assertEquals(100L, (int) request.getAttribute("viewed"));
        
    }
    
    @Test
    public void testGetMostViewedWithNoId() {
        VideoController videoController = new VideoController();
        MockHttpServletRequest request = new MockHttpServletRequest();
        request.setRequestURI("/mostViewed");
        MockHttpServletResponse response = new MockHttpServletResponse();

        // must invoke
        videoController.getMostViewed(request, response);
        
        assertEquals(500, response.getStatus());
        assertNull(request.getAttribute("viewed"));        
    }
}

testGetMostViewed()方法實例化VideoController類並建立兩個mock對象,一個MockHttpServletRequest和一個MockHttpServletResponse。它還設置請求URI,並向MockHttpServletRequest添加屬性「id」。

   VideoController videoController = new VideoController();
     MockHttpServletRequest request = new MockHttpServletRequest();
     request.setRequestURI("/mostViewed");
     request.setAttribute("id", 1);
     MockHttpServletResponse response = new MockHttpServletResponse();

而後調用VideoController的getMostViewed()方法,傳遞mock對象,而後驗證響應的狀態碼爲200,請求包含一個值爲100的「viewed」屬性:

        // must invoke
        videoController.getMostViewed(request, response);
        
        assertEquals(200, response.getStatus());
        assertEquals(100L, (int) request.getAttribute("viewed"));

VideoControllerTest的第二個方法相似方法一,但不會向MockHttpServletRequest對象添加"id"屬性。所以,在調用控制器的方法時,它接收HTTP響應狀態代碼500,而且在MockHttpServletRequest對象中沒有「viewed」屬性。

二、ModelAndViewAssert

ModelAndViewAssert類是org.springframework.test.web包的一部分,是另外一個有用的Spring類,用於測試模型從控制器請求處理方法返回的ModelAndView。在Spring MVC -- 基於註解的控制器中介紹過,ModelAndView是請求處理方法能夠返回獲得類型之一,該類型包含有關請求方法的模型和視圖信息,其中模型是用來提供給目標視圖,用於界面顯示的。

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.web;

import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.ModelAndView;

import static org.springframework.test.util.AssertionErrors.assertTrue;
import static org.springframework.test.util.AssertionErrors.fail;

/**
 * A collection of assertions intended to simplify testing scenarios dealing
 * with Spring Web MVC {@link org.springframework.web.servlet.ModelAndView
 * ModelAndView} objects.
 *
 * <p>Intended for use with JUnit 4 and TestNG. All {@code assert*()} methods
 * throw {@link AssertionError AssertionErrors}.
 *
 * @author Sam Brannen
 * @author Alef Arendsen
 * @author Bram Smeets
 * @since 2.5
 * @see org.springframework.web.servlet.ModelAndView
 */
public abstract class ModelAndViewAssert {

    /**
     * Checks whether the model value under the given {@code modelName}
     * exists and checks it type, based on the {@code expectedType}. If the
     * model entry exists and the type matches, the model value is returned.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param modelName name of the object to add to the model (never {@code null})
     * @param expectedType expected type of the model value
     * @return the model value
     */
    @SuppressWarnings("unchecked")
    public static <T> T assertAndReturnModelAttributeOfType(ModelAndView mav, String modelName, Class<T> expectedType) {
        Map<String, Object> model = mav.getModel();
        Object obj = model.get(modelName);
        if (obj == null) {
            fail("Model attribute with name '" + modelName + "' is null");
        }
        assertTrue("Model attribute is not of expected type '" + expectedType.getName() + "' but rather of type '" +
                obj.getClass().getName() + "'", expectedType.isAssignableFrom(obj.getClass()));
        return (T) obj;
    }

    /**
     * Compare each individual entry in a list, without first sorting the lists.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param modelName name of the object to add to the model (never {@code null})
     * @param expectedList the expected list
     */
    @SuppressWarnings("rawtypes")
    public static void assertCompareListModelAttribute(ModelAndView mav, String modelName, List expectedList) {
        List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class);
        assertTrue("Size of model list is '" + modelList.size() + "' while size of expected list is '" +
                expectedList.size() + "'", expectedList.size() == modelList.size());
        assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.",
                expectedList.equals(modelList));
    }

    /**
     * Assert whether or not a model attribute is available.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param modelName name of the object to add to the model (never {@code null})
     */
    public static void assertModelAttributeAvailable(ModelAndView mav, String modelName) {
        Map<String, Object> model = mav.getModel();
        assertTrue("Model attribute with name '" + modelName + "' is not available", model.containsKey(modelName));
    }

    /**
     * Compare a given {@code expectedValue} to the value from the model
     * bound under the given {@code modelName}.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param modelName name of the object to add to the model (never {@code null})
     * @param expectedValue the model value
     */
    public static void assertModelAttributeValue(ModelAndView mav, String modelName, Object expectedValue) {
        Object modelValue = assertAndReturnModelAttributeOfType(mav, modelName, Object.class);
        assertTrue("Model value with name '" + modelName + "' is not the same as the expected value which was '" +
                expectedValue + "'", modelValue.equals(expectedValue));
    }

    /**
     * Inspect the {@code expectedModel} to see if all elements in the
     * model appear and are equal.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param expectedModel the expected model
     */
    public static void assertModelAttributeValues(ModelAndView mav, Map<String, Object> expectedModel) {
        Map<String, Object> model = mav.getModel();

        if (!model.keySet().equals(expectedModel.keySet())) {
            StringBuilder sb = new StringBuilder("Keyset of expected model does not match.\n");
            appendNonMatchingSetsErrorMessage(expectedModel.keySet(), model.keySet(), sb);
            fail(sb.toString());
        }

        StringBuilder sb = new StringBuilder();
        model.forEach((modelName, mavValue) -> {
            Object assertionValue = expectedModel.get(modelName);
            if (!assertionValue.equals(mavValue)) {
                sb.append("Value under name '").append(modelName).append("' differs, should have been '").append(
                    assertionValue).append("' but was '").append(mavValue).append("'\n");
            }
        });

        if (sb.length() != 0) {
            sb.insert(0, "Values of expected model do not match.\n");
            fail(sb.toString());
        }
    }

    /**
     * Compare each individual entry in a list after having sorted both lists
     * (optionally using a comparator).
     * @param mav the ModelAndView to test against (never {@code null})
     * @param modelName name of the object to add to the model (never {@code null})
     * @param expectedList the expected list
     * @param comparator the comparator to use (may be {@code null}). If not
     * specifying the comparator, both lists will be sorted not using any comparator.
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    public static void assertSortAndCompareListModelAttribute(
            ModelAndView mav, String modelName, List expectedList, Comparator comparator) {

        List modelList = assertAndReturnModelAttributeOfType(mav, modelName, List.class);
        assertTrue("Size of model list is '" + modelList.size() + "' while size of expected list is '" +
                expectedList.size() + "'", expectedList.size() == modelList.size());

        modelList.sort(comparator);
        expectedList.sort(comparator);

        assertTrue("List in model under name '" + modelName + "' is not equal to the expected list.",
                expectedList.equals(modelList));
    }

    /**
     * Check to see if the view name in the ModelAndView matches the given
     * {@code expectedName}.
     * @param mav the ModelAndView to test against (never {@code null})
     * @param expectedName the name of the model value
     */
    public static void assertViewName(ModelAndView mav, String expectedName) {
        assertTrue("View name is not equal to '" + expectedName + "' but was '" + mav.getViewName() + "'",
                ObjectUtils.nullSafeEquals(expectedName, mav.getViewName()));
    }


    private static void appendNonMatchingSetsErrorMessage(
            Set<String> assertionSet, Set<String> incorrectSet, StringBuilder sb) {

        Set<String> tempSet = new HashSet<>(incorrectSet);
        tempSet.removeAll(assertionSet);

        if (!tempSet.isEmpty()) {
            sb.append("Set has too many elements:\n");
            for (Object element : tempSet) {
                sb.append('-');
                sb.append(element);
                sb.append('\n');
            }
        }

        tempSet = new HashSet<>(assertionSet);
        tempSet.removeAll(incorrectSet);

        if (!tempSet.isEmpty()) {
            sb.append("Set is missing elements:\n");
            for (Object element : tempSet) {
                sb.append('-');
                sb.append(element);
                sb.append('\n');
            }
        }
    }

}
View Code

下表給出ModelAndViewAssert的一些主要方法:

方法 描述
assertViewName 檢查ModelAndView的視圖名稱是都與預期名稱匹配
assertModelAttributeValue 檢查ModelAndView的模型是否包含具備指定名稱和值的屬性
assertModelAttributeAvailable 檢查ModelAndView的模型是否包含具備指定名稱的屬性
assertSortAndCompareListModelAttribute 對ModelAndView的模型列表屬性進行排序,而後將其與預期列表進行比較
assertAndReturnModelAttributeOfType 檢查ModelAndView的模型是否包含具備指定名稱和類型的屬性

考慮一個Book實體類,有4個屬性,isbn、title、author和pubDate:

package com.example.model;

import java.time.LocalDate;

public class Book {
    private String isbn;
    private String title;
    private String author;
    private LocalDate pubDate;
    
    public Book(String isbn, LocalDate pubDate) {
        this.isbn = isbn;
        this.pubDate = pubDate;
    }
    
    public Book(String isbn, String title, String author,
            LocalDate pubDate) {
        this.isbn = isbn;
        this.title = title;
        this.author = author;
        this.pubDate = pubDate;
    }
    
    public String getIsbn() {
        return isbn;
    }
    public void setIsbn(String isbn) {
        this.isbn = isbn;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getAuthor() {
        return author;
    }
    public void setAuthor(String author) {
        this.author = author;
    }
    public LocalDate getPubDate() {
        return pubDate;
    }
    public void setPubDate(LocalDate pubDate) {
        this.pubDate = pubDate;
    }
    
    @Override
    public boolean equals(Object otherBook) {
        return isbn.equals(((Book)otherBook).getIsbn());
    }
}

建立一個Spring MVC控制器BookController,它包含一個請求處理方法getLatestTitles(),該方法接受putYear路徑變量,並返回一個ModelAndView,若是putYear值爲「2016」,它將包含書籍列表:

package com.example.controller;

import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.example.model.Book;

@Controller
public class BookController {
    @RequestMapping(value = "/latest/{pubYear}")
    public ModelAndView getLatestTitles(
            @PathVariable String pubYear) {
        ModelAndView mav = new ModelAndView("Latest Titles");
        
        if ("2016".equals(pubYear)) {
            List<Book> list = Arrays.asList(
                    new Book("0001", "Spring MVC: A Tutorial", 
                            "Paul Deck", 
                            LocalDate.of(2016, 6, 1)),
                    new Book("0002", "Java Tutorial",
                            "Budi Kurniawan", 
                            LocalDate.of(2016, 11, 1)),
                    new Book("0003", "SQL", "Will Biteman", 
                            LocalDate.of(2016, 12, 12)));
            mav.getModel().put("latest", list);
        }
        return mav;
    }
}

測試BookController控制器的一種簡單方式是使用ModelAndViewAssert中的靜態方法,咱們建立一個測試類BookControllerTest:

package com.example.controller;

import static org.springframework.test.web.ModelAndViewAssert.*;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import org.junit.Test;
import org.springframework.web.servlet.ModelAndView;

import com.example.model.Book;

public class BookControllerTest {
    @Test
    public void test() {
        BookController bookController = new BookController();
        ModelAndView mav = bookController
                .getLatestTitles("2016");
        assertViewName(mav, "Latest Titles");
        assertModelAttributeAvailable(mav, "latest");
        List<Book> expectedList = Arrays.asList(
                new Book("0002", LocalDate.of(2016, 11, 1)),
                new Book("0001", LocalDate.of(2016, 6, 1)),
                new Book("0003", LocalDate.of(2016, 12, 12)));
        assertAndReturnModelAttributeOfType(mav, "latest", 
                expectedList.getClass());
        Comparator<Book> pubDateComparator = 
                (a, b) -> a.getPubDate()
                .compareTo(b.getPubDate());
        assertSortAndCompareListModelAttribute(mav, "latest", 
                expectedList, pubDateComparator);
    }
}

assertSortAndCompareListModelAttribute()方法的第4個參數須要傳入一個比較器對象,其實現了Comparator接口。

四 應用Spring Test進行集成測試

集成測試用來測試不一樣的模塊是否能夠一塊兒工做。它還確保兩個模塊之間數據的傳遞,使用Spring框架依賴注入容器,必須檢查bean依賴注入。

若沒有合適的工具,集成測試可能須要不少時間。想一想一下,若是你正在創建一個網上商店,你必須使用瀏覽器來測試購物車是否正確計算。每次更改代碼,你必須從新啓動瀏覽器,登陸系統,將幾個項目條件到購物車,並檢查總數是否正確,每次迭代會花費幾分鐘。

好在,Spring提供了一個用於集成測試的模塊:Spring Test。

Spring的MockHttpServletRequest、MockHttpServletResponse、MockHttpSession類適用於對Spring MVC控制器進行單元測試,但它們缺乏與集成測試相關的功能。例如,它們直接調用請求處理方法,沒法測試請求映射和數據綁定。它們也不測試bean依賴注入,由於SUV類使用new運算符實例化。

對於集成測試,你須要一組不一樣的Spring MVC測試類型。如下小結討論集成測試的API,並提供一個示例。

一、API

做爲Spring的一個模塊,Spring Test提供了一些實用類,能夠放的在Spring MVC應用程序上執行集成測試。bean是使用Spring依賴注入器建立的,並從ApplicationContext中獲取(ApplicationContext表明一個Spring反轉控制容器),就像在一個真正的Spring應用程序中同樣。

MockMvc類位於org.springframework.test.web.servlet包下,是Spring Test中的主類,用於幫助集成測試。此類容許你使用預約義的請求映射來調用請求處理方法。

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.web.servlet;

import java.util.ArrayList;
import java.util.List;
import javax.servlet.AsyncContext;
import javax.servlet.DispatcherType;
import javax.servlet.Filter;
import javax.servlet.ServletContext;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

import org.springframework.beans.Mergeable;
import org.springframework.lang.Nullable;
import org.springframework.mock.web.MockFilterChain;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.util.Assert;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.DispatcherServlet;

/**
 * <strong>Main entry point for server-side Spring MVC test support.</strong>
 *
 * <h3>Example</h3>
 *
 * <pre class="code">
 * import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
 * import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
 * import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
 *
 * // ...
 *
 * WebApplicationContext wac = ...;
 *
 * MockMvc mockMvc = webAppContextSetup(wac).build();
 *
 * mockMvc.perform(get("/form"))
 *     .andExpect(status().isOk())
 *     .andExpect(content().mimeType("text/html"))
 *     .andExpect(forwardedUrl("/WEB-INF/layouts/main.jsp"));
 * </pre>
 *
 * @author Rossen Stoyanchev
 * @author Rob Winch
 * @author Sam Brannen
 * @since 3.2
 */
public final class MockMvc {

    static final String MVC_RESULT_ATTRIBUTE = MockMvc.class.getName().concat(".MVC_RESULT_ATTRIBUTE");

    private final TestDispatcherServlet servlet;

    private final Filter[] filters;

    private final ServletContext servletContext;

    @Nullable
    private RequestBuilder defaultRequestBuilder;

    private List<ResultMatcher> defaultResultMatchers = new ArrayList<>();

    private List<ResultHandler> defaultResultHandlers = new ArrayList<>();


    /**
     * Private constructor, not for direct instantiation.
     * @see org.springframework.test.web.servlet.setup.MockMvcBuilders
     */
    MockMvc(TestDispatcherServlet servlet, Filter... filters) {
        Assert.notNull(servlet, "DispatcherServlet is required");
        Assert.notNull(filters, "Filters cannot be null");
        Assert.noNullElements(filters, "Filters cannot contain null values");

        this.servlet = servlet;
        this.filters = filters;
        this.servletContext = servlet.getServletContext();
    }


    /**
     * A default request builder merged into every performed request.
     * @see org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder#defaultRequest(RequestBuilder)
     */
    void setDefaultRequest(@Nullable RequestBuilder requestBuilder) {
        this.defaultRequestBuilder = requestBuilder;
    }

    /**
     * Expectations to assert after every performed request.
     * @see org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder#alwaysExpect(ResultMatcher)
     */
    void setGlobalResultMatchers(List<ResultMatcher> resultMatchers) {
        Assert.notNull(resultMatchers, "ResultMatcher List is required");
        this.defaultResultMatchers = resultMatchers;
    }

    /**
     * General actions to apply after every performed request.
     * @see org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder#alwaysDo(ResultHandler)
     */
    void setGlobalResultHandlers(List<ResultHandler> resultHandlers) {
        Assert.notNull(resultHandlers, "ResultHandler List is required");
        this.defaultResultHandlers = resultHandlers;
    }

    /**
     * Return the underlying {@link DispatcherServlet} instance that this
     * {@code MockMvc} was initialized with.
     * <p>This is intended for use in custom request processing scenario where a
     * request handling component happens to delegate to the {@code DispatcherServlet}
     * at runtime and therefore needs to be injected with it.
     * <p>For most processing scenarios, simply use {@link MockMvc#perform},
     * or if you need to configure the {@code DispatcherServlet}, provide a
     * {@link DispatcherServletCustomizer} to the {@code MockMvcBuilder}.
     * @since 5.1
     */
    public DispatcherServlet getDispatcherServlet() {
        return this.servlet;
    }


    /**
     * Perform a request and return a type that allows chaining further
     * actions, such as asserting expectations, on the result.
     * @param requestBuilder used to prepare the request to execute;
     * see static factory methods in
     * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders}
     * @return an instance of {@link ResultActions} (never {@code null})
     * @see org.springframework.test.web.servlet.request.MockMvcRequestBuilders
     * @see org.springframework.test.web.servlet.result.MockMvcResultMatchers
     */
    public ResultActions perform(RequestBuilder requestBuilder) throws Exception {
        if (this.defaultRequestBuilder != null && requestBuilder instanceof Mergeable) {
            requestBuilder = (RequestBuilder) ((Mergeable) requestBuilder).merge(this.defaultRequestBuilder);
        }

        MockHttpServletRequest request = requestBuilder.buildRequest(this.servletContext);

        AsyncContext asyncContext = request.getAsyncContext();
        MockHttpServletResponse mockResponse;
        HttpServletResponse servletResponse;
        if (asyncContext != null) {
            servletResponse = (HttpServletResponse) asyncContext.getResponse();
            mockResponse = unwrapResponseIfNecessary(servletResponse);
        }
        else {
            mockResponse = new MockHttpServletResponse();
            servletResponse = mockResponse;
        }

        if (requestBuilder instanceof SmartRequestBuilder) {
            request = ((SmartRequestBuilder) requestBuilder).postProcessRequest(request);
        }

        final MvcResult mvcResult = new DefaultMvcResult(request, mockResponse);
        request.setAttribute(MVC_RESULT_ATTRIBUTE, mvcResult);

        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, servletResponse));

        MockFilterChain filterChain = new MockFilterChain(this.servlet, this.filters);
        filterChain.doFilter(request, servletResponse);

        if (DispatcherType.ASYNC.equals(request.getDispatcherType()) &&
                asyncContext != null && !request.isAsyncStarted()) {
            asyncContext.complete();
        }

        applyDefaultResultActions(mvcResult);
        RequestContextHolder.setRequestAttributes(previousAttributes);

        return new ResultActions() {
            @Override
            public ResultActions andExpect(ResultMatcher matcher) throws Exception {
                matcher.match(mvcResult);
                return this;
            }
            @Override
            public ResultActions andDo(ResultHandler handler) throws Exception {
                handler.handle(mvcResult);
                return this;
            }
            @Override
            public MvcResult andReturn() {
                return mvcResult;
            }
        };
    }

    private MockHttpServletResponse unwrapResponseIfNecessary(ServletResponse servletResponse) {
        while (servletResponse instanceof HttpServletResponseWrapper) {
            servletResponse = ((HttpServletResponseWrapper) servletResponse).getResponse();
        }
        Assert.isInstanceOf(MockHttpServletResponse.class, servletResponse);
        return (MockHttpServletResponse) servletResponse;
    }

    private void applyDefaultResultActions(MvcResult mvcResult) throws Exception {
        for (ResultMatcher matcher : this.defaultResultMatchers) {
            matcher.match(mvcResult);
        }
        for (ResultHandler handler : this.defaultResultHandlers) {
            handler.handle(mvcResult);
        }
    }

}
View Code

這是一種常見的建立MocklMvc實例的方法:

MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();

這裏webAppContext是WebApplicationContext實例的一個引用,WebApplicationContext是ApplicationContext的子類,每一個Spring開發人員都應該熟悉。要獲取一個WebApplicationContext,你必須在測試類中聲明這一點,即應用@Autowired進行依賴注入:

    @Autowired
    private WebApplicationContext webAppContext;

MockMvcBuilders讀取一個爲測試類定義的配置文件(通常爲Spring配置文件)。後面的示例程序將演示如何指定測試類的配置文件,在這以前,咱們先討論MockMvc。

MockMvc是一個很是簡單的類。事實上,它只有一個方法:perform(),用於經過URI間接調用Spring MVC控制器。

perform()方法具備如下簽名:

public ResultActions perform(RequestBuilder requestBuilder) 

要測試請求處理方法,你須要建立一個RequestBuilder。好在,MockMvcRequestBuilders類提供了與HTTP method具備相同名稱的靜態方法:get()、post()、head()、put()、patch()、delete()等。要使用HTTP GET方法測試控制器,你能夠調用get()靜態方法,要使用HTTP POST方法測試,則調用post(0靜態方法。這些靜態方法也很容易使用,你只須要傳遞一個字符串——控制器的請求處理方法的URI。

例如,要調用名爲getEmployee的請求處理方法,你將編寫以下代碼:

ResultActions resultActions = mockMvc.perform(get("getRmployee"));

固然,你必須導入MockMvcRequestBuilders的靜態get()方法:

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

要驗證測試是否成功,你須要調用ResultActions的andExpect ()方法,andExpect()方法簽名以下:

ResultActions andExpect(ResultMatcher matcher) 

注意:andExpect()返回ResultActions的另外一個實例,這意味着能夠鏈式調用多個andExpect()方法。

MockMvcResultMatchers類提供了靜態方法來輕鬆建立ResultMatcher。

/*
 * Copyright 2002-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.springframework.test.web.servlet.result;

import java.util.Map;
import javax.xml.xpath.XPathExpressionException;

import org.hamcrest.Matcher;

import org.springframework.lang.Nullable;
import org.springframework.test.web.servlet.ResultMatcher;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.util.UriComponentsBuilder;

import static org.springframework.test.util.AssertionErrors.assertEquals;
import static org.springframework.test.util.AssertionErrors.assertTrue;

/**
 * Static factory methods for {@link ResultMatcher}-based result actions.
 *
 * <h3>Eclipse Users</h3>
 * <p>Consider adding this class as a Java editor favorite. To navigate to
 * this setting, open the Preferences and type "favorites".
 *
 * @author Rossen Stoyanchev
 * @author Brian Clozel
 * @author Sam Brannen
 * @since 3.2
 */
public abstract class MockMvcResultMatchers {

    private static final AntPathMatcher pathMatcher = new AntPathMatcher();


    /**
     * Access to request-related assertions.
     */
    public static RequestResultMatchers request() {
        return new RequestResultMatchers();
    }

    /**
     * Access to assertions for the handler that handled the request.
     */
    public static HandlerResultMatchers handler() {
        return new HandlerResultMatchers();
    }

    /**
     * Access to model-related assertions.
     */
    public static ModelResultMatchers model() {
        return new ModelResultMatchers();
    }

    /**
     * Access to assertions on the selected view.
     */
    public static ViewResultMatchers view() {
        return new ViewResultMatchers();
    }

    /**
     * Access to flash attribute assertions.
     */
    public static FlashAttributeResultMatchers flash() {
        return new FlashAttributeResultMatchers();
    }

    /**
     * Asserts the request was forwarded to the given URL.
     * <p>This method accepts only exact matches.
     * @param expectedUrl the exact URL expected
     */
    public static ResultMatcher forwardedUrl(@Nullable String expectedUrl) {
        return result -> assertEquals("Forwarded URL", expectedUrl, result.getResponse().getForwardedUrl());
    }

    /**
     * Asserts the request was forwarded to the given URL template.
     * <p>This method accepts exact matches against the expanded and encoded URL template.
     * @param urlTemplate a URL template; the expanded URL will be encoded
     * @param uriVars zero or more URI variables to populate the template
     * @see UriComponentsBuilder#fromUriString(String)
     */
    public static ResultMatcher forwardedUrlTemplate(String urlTemplate, Object... uriVars) {
        String uri = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(uriVars).encode().toUriString();
        return forwardedUrl(uri);
    }

    /**
     * Asserts the request was forwarded to the given URL.
     * <p>This method accepts {@link org.springframework.util.AntPathMatcher}
     * patterns.
     * @param urlPattern an AntPath pattern to match against
     * @since 4.0
     * @see org.springframework.util.AntPathMatcher
     */
    public static ResultMatcher forwardedUrlPattern(String urlPattern) {
        return result -> {
            assertTrue("AntPath pattern", pathMatcher.isPattern(urlPattern));
            String url = result.getResponse().getForwardedUrl();
            assertTrue("Forwarded URL does not match the expected URL pattern",
                    (url != null && pathMatcher.match(urlPattern, url)));
        };
    }

    /**
     * Asserts the request was redirected to the given URL.
     * <p>This method accepts only exact matches.
     * @param expectedUrl the exact URL expected
     */
    public static ResultMatcher redirectedUrl(String expectedUrl) {
        return result -> assertEquals("Redirected URL", expectedUrl, result.getResponse().getRedirectedUrl());
    }

    /**
     * Asserts the request was redirected to the given URL template.
     * <p>This method accepts exact matches against the expanded and encoded URL template.
     * @param urlTemplate a URL template; the expanded URL will be encoded
     * @param uriVars zero or more URI variables to populate the template
     * @see UriComponentsBuilder#fromUriString(String)
     */
    public static ResultMatcher redirectedUrlTemplate(String urlTemplate, Object... uriVars) {
        String uri = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(uriVars).encode().toUriString();
        return redirectedUrl(uri);
    }

    /**
     * Asserts the request was redirected to the given URL.
     * <p>This method accepts {@link org.springframework.util.AntPathMatcher}
     * patterns.
     * @param urlPattern an AntPath pattern to match against
     * @since 4.0
     * @see org.springframework.util.AntPathMatcher
     */
    public static ResultMatcher redirectedUrlPattern(String urlPattern) {
        return result -> {
            assertTrue("No Ant-style path pattern", pathMatcher.isPattern(urlPattern));
            String url = result.getResponse().getRedirectedUrl();
            assertTrue("Redirected URL does not match the expected URL pattern",
                    (url != null && pathMatcher.match(urlPattern, url)));
        };
    }

    /**
     * Access to response status assertions.
     */
    public static StatusResultMatchers status() {
        return new StatusResultMatchers();
    }

    /**
     * Access to response header assertions.
     */
    public static HeaderResultMatchers header() {
        return new HeaderResultMatchers();
    }

    /**
     * Access to response body assertions.
     */
    public static ContentResultMatchers content() {
        return new ContentResultMatchers();
    }

    /**
     * Access to response body assertions using a
     * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
     * to inspect a specific subset of the body.
     * <p>The JSON path expression can be a parameterized string using
     * formatting specifiers as defined in
     * {@link String#format(String, Object...)}.
     * @param expression the JSON path expression, optionally parameterized with arguments
     * @param args arguments to parameterize the JSON path expression with
     */
    public static JsonPathResultMatchers jsonPath(String expression, Object... args) {
        return new JsonPathResultMatchers(expression, args);
    }

    /**
     * Access to response body assertions using a
     * <a href="https://github.com/jayway/JsonPath">JsonPath</a> expression
     * to inspect a specific subset of the body and a Hamcrest matcher for
     * asserting the value found at the JSON path.
     * @param expression the JSON path expression
     * @param matcher a matcher for the value expected at the JSON path
     */
    public static <T> ResultMatcher jsonPath(String expression, Matcher<T> matcher) {
        return new JsonPathResultMatchers(expression).value(matcher);
    }

    /**
     * Access to response body assertions using an XPath expression to
     * inspect a specific subset of the body.
     * <p>The XPath expression can be a parameterized string using formatting
     * specifiers as defined in {@link String#format(String, Object...)}.
     * @param expression the XPath expression, optionally parameterized with arguments
     * @param args arguments to parameterize the XPath expression with
     */
    public static XpathResultMatchers xpath(String expression, Object... args) throws XPathExpressionException {
        return new XpathResultMatchers(expression, null, args);
    }

    /**
     * Access to response body assertions using an XPath expression to
     * inspect a specific subset of the body.
     * <p>The XPath expression can be a parameterized string using formatting
     * specifiers as defined in {@link String#format(String, Object...)}.
     * @param expression the XPath expression, optionally parameterized with arguments
     * @param namespaces namespaces referenced in the XPath expression
     * @param args arguments to parameterize the XPath expression with
     */
    public static XpathResultMatchers xpath(String expression, Map<String, String> namespaces, Object... args)
            throws XPathExpressionException {

        return new XpathResultMatchers(expression, namespaces, args);
    }

    /**
     * Access to response cookie assertions.
     */
    public static CookieResultMatchers cookie() {
        return new CookieResultMatchers();
    }

}
View Code

MockMvcResultMatchers屬於org.springframework.test.web.servlet.result包。下表顯示了它的一些方法:

方法 返回類型 描述
cookie CookieResultMatchers 返回一個ResultMatchers,用來斷言cookie值
header HeaderResultMatchers 返回一個ResultMatchers,用來斷言HTTP香影頭部
model ModelResultMatchers 返回一個ResultMatchers,用來斷言請求處理的模型
status StatusResultMatchers 返回一個ResultMatchers,用來斷言HTTP響應狀態
view ViewResultMatchers 返回一個ResultMatchers,用來斷言請求處理的視圖

例如,要確保控制器方法的請求映射正確,可使用狀態方法:

mockMvc.perform(get("/getBook")).andExpect(status().isOk());

你必須導入MockMvcResultMatchers的靜態status()方法:

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

isOK()方法斷言響應狀態碼是200:

    /**
     * Assert the response status code is {@code HttpStatus.OK} (200).
     */
    public ResultMatcher isOk() {
        return matcher(HttpStatus.OK);
    }

能夠看到MockMvc及其相關類使得集成測試控制器變得很是容易。

二、Spring Test測試類的框架

瞭解了Spring Test中的一些重要的API,如今來看下Spring MVC測試類的框架:

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("...")
public class ProductControllerTest {
    @Autowired
    private WebApplicationContext webAppContext;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
    }
    
    @After
    public void cleanUp() {
        
    }

    @Test
    public void test1() throws Exception {
        mockMvc.perform(...) .andExpect(...);
    }

    @Test
    public void test2() throws Exception {
        mockMvc.perform(...) .andExpect(...);
    }
}

首先,看下你要導入的類型。在導入列表的頂部,是來自JUnit和Spring Test的類型。像單元測試類同樣,Spring MVC測試類能夠包括用@Before和@After註解的方法。兩種註解類型都是JUnit的一部分。

接下去,測試框架開始與單元測試有所不用。首先是測試類運行期。你須要一個SpringJUnit4ClassRunner.class在@RunWith註解內:

@RunWith(SpringJUnit4ClassRunner.class)

這個runner容許你使用Spring。而後,你須要添加以下註解類型:

@WebAppConfiguration
@ContextConfiguration("...")

WebAppConfiguration註解類型用於聲明爲集成測試加載的ApplicationContext應該是WebApplicationContext類型。ContextConfiguration註解類型告訴測試運行器如何加載和配置WebApplicationContext。

此外,測試類中還須要兩個對象:

    @Autowired
    private WebApplicationContext webAppContext;
    private MockMvc mockMvc;

三、示例

如下示例展現如何對Spring MVC控制器開展集成測試。integration-test應用目錄結構以下:

test-config.xml配置文件展現了將要被掃描的包。這個文件是一個典型的Spring MVC配置文件,可是去除了任何資源映射和視圖解析器。可是,你可使用實際的配置文件:

<?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:p="http://www.springframework.org/schema/p"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/mvc
        http://www.springframework.org/schema/mvc/spring-mvc.xsd     
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="controller"/>
    <context:component-scan base-package="service"/>    
    <mvc:annotation-driven/>
</beans>

控制器EmployeeController類,只有一個請求處理方法getHighestPaid(),它映射到/highest-paid:

package controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import service.EmployeeService;
import domain.Employee;

@Controller
public class EmployeeController {
    
    @Autowired
    EmployeeService employeeService;
    
    @RequestMapping(value="/highest-paid/{category}")
    public String getHighestPaid(@PathVariable int category, Model model) {
        Employee employee = employeeService.getHighestPaidEmployee(category);
        model.addAttribute("employee", employee);
        return "success";
    }
}

EmployeeControllerTest是控制器EmployeeController的測試類:

package com.example.controller;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration("test-config.xml")
public class EmployeeControllerTest {
    @Autowired
    private WebApplicationContext webAppContext;

    private MockMvc mockMvc;

    @Before
    public void setup() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
    }
    
    @After
    public void cleanUp() {
        
    }

    @Test
    public void testGetHighestPaidEmployee() throws Exception {
        mockMvc.perform(get("/highest-paid/2"))
                .andExpect(status().isOk())
                .andExpect(model().attributeExists("employee"))
                .andDo(print());
    }
}

EmployeeControllerTest類包含一個setup()方法,它建立一個MockMvc對象,testGetHighestPaidEmployee()方法執行測試,並指望響應狀態代碼爲200,而且模型具備employee屬性。

測試方法還調用了andDo(print())在響應對象中打印各類值,若是測試成功經過,則能夠看到相似結果:

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /highest-paid/2
       Parameters = {}
          Headers = []
             Body = <no character encoding set>
    Session Attrs = {}

Handler:
             Type = controller.EmployeeController
           Method = public java.lang.String controller.EmployeeController.getHighestPaid(int,org.springframework.ui.Model)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = success
             View = null
        Attribute = employee
            value = Xiao Ming ($200000)
           errors = []

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Language:"en"]
     Content type = null
             Body = 
    Forwarded URL = success
   Redirected URL = null
          Cookies = []

其它一些不過重要的類:

Employee類:

package domain;

import java.math.BigDecimal;

public class Employee {
    private String name;
    private BigDecimal salary;
    
    public Employee(String name, BigDecimal salary) {
        this.name = name;
        this.salary = salary;
    }
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public BigDecimal getSalary() {
        return salary;
    }
    public void setSalary(BigDecimal salary) {
        this.salary = salary;
    }
    
    @Override
    public String toString() {
        return name + " ($" + salary + ")"; 
    }
}
View Code

EmployeeService類:

package service;

import domain.Employee;

public interface EmployeeService {
    Employee getHighestPaidEmployee(int employeeCategory);
}
View Code

EmployeeServiceImpl類

package service;

import java.math.BigDecimal;

import org.springframework.stereotype.Service;

import domain.Employee;

@Service
public class EmployeeServiceImpl implements EmployeeService {

    public Employee getHighestPaidEmployee(int employeeCategory) {
        switch (employeeCategory) {
        case 1:
            return new Employee("Alicia Coder", new BigDecimal(123_000));
        case 2:
            return new Employee("Xiao Ming", new BigDecimal(200_000));
        default:
            return new Employee("Jay Boss", new BigDecimal(400_000));
        }
    };
}
View Code

五 總結

測試是軟件開發中的一個重要步驟,你應該在開發週期中儘早的執行單元測試和集成測試這兩種類型的測試:

  • 單元測試用於類的功能性驗證。在單元測試中,所涉及依賴一般被測試擋板替換,其能夠包括dummy、stub、spy、fake和mock對象。JUnit是一個流行的用於單元測試的框架,而且一般與mock框架(如Mockito和EasyMock)結合使用;
  • 集成測試用於確保同一個應用程序中的不一樣模塊能夠一塊兒工做,同時確保請求映射和數據綁定也能夠工做。Spring Test是一個Spring模塊,它提供一組API,能夠輕鬆的對Spring應用程序執行集成測試。

參考文章

[1] Spring MVC學習指南

相關文章
相關標籤/搜索