程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)

Spring MVC -- 单元测试和集成测试

测试在软件开发中的重要性不言而喻。测试的主要目的是尽早发现错误,最好是在代码开发的同时。逻辑上认为,错误发现的越早,修复的成本越低。如果在编程中发现错误,可以立即更改代码;如果软件发布后,客户发现错误所需要的修复成本会很大。

在软件开发中有许多不同的测试,其中两个是单元测试和集成测试。通常从单元测试开始测试类中的单个方法,然后进行集成测试,以测试不同的模块是否可以无缝协同工作。

本篇博客中的示例使用JUnit测试框架以及Spring test模块。Spring test模块中的API可用于单元测试和集成测试。可以在org.springframework.test及其子包以及org.springframework.mock.*包中找到Spring测试相关的类型。

一 单元测试

单元测试的理想情况下是为每个类创建一个测试类,并为类中的每个方法创建一个测试方法,像getter和setter这样的简单方法除外,他们直接从字段返回值或赋值给字段。

在测试语中,被测试的类称为被测系统(SUT)。

单元测试旨在快速且多次运行。单元测试仅验证代码本身,而不涉及它的依赖,其任何依赖应该被帮助对象代替。设计依赖项的测试通常在集成测试中完成,而不是在单元测试。

你可能会问,我们可以使用main()方法从类本身内测试一个类,为什么还需要单元测试内?这主要是因为,单元测试具有以下好处:

  • 在单独测试类在编写测试代码不会混淆你的类;
  • 单元测试可以用于回归测试,在一些逻辑发生变化时,以确保一切仍然工作;
  • 单元测试可以在持续集成设置中自动化测试;持续集成是指一种开发方法,当程序员将他们的代码提交到共享库时,每次代码提交将触发一次自动构建并运行所有单元测试,持续集成可以尽早的检测问题。

在单元测试中,类使用new运算符实例化。不依赖Spring框架的依赖注入容易来创建bean。

下面我们创建一个被测试类MyUtility:

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

为了对这个类进行测试,创建一个MyUtilityTest类,注意每个方法应该至少有一个测试方法:

复制代码
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的标准单元测试框架。

1、应用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的信息和使用,可以阅读官方文档。

2、开发一个单元测试

编写单元测试很简单,使用@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()方法用来能比较两个值。

3、运行一个单元测试 

Eclipse知道一个类是否是一个JUnit测试类。要运行测试类,请右键单击包资源管理器中的测试类,然后选择运行方式Run As JUnit Test。

测试完成后,如果JUnit视图尚未打开,Eclipse将打开它。如果单元测试成功完成后,将会在JUnit视图中看到一个绿色条:

4、通过测试套件来运行

在有十几个类的小项目中,你将有十几个测试类。在一个更大的项目中,你会有更多测试类。在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;

这些类型中的每一种将在下面的小节中解释。

1、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
复制代码

2、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);
    }

}
复制代码

3、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。

4、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());
    }
}
复制代码

5、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而构建的,并且与真实对象更接近,更容易使用,以下讨论其中一些重要的单元测试控制器类型。

1、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”属性。

2、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,并提供一个示例。

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

日期姓名金额
2023-09-06*源19
2023-09-11*朝科88
2023-09-21*号5
2023-09-16*真60
2023-10-26*通9.9
2023-11-04*慎0.66
2023-11-24*恩0.01
2023-12-30I*B1
2024-01-28*兴20
2024-02-01QYing20
2024-02-11*督6
2024-02-18一*x1
2024-02-20c*l18.88
2024-01-01*I5
2024-04-08*程150
2024-04-18*超20
2024-04-26.*V30
2024-05-08D*W5
2024-05-29*辉20
2024-05-30*雄10
2024-06-08*:10
2024-06-23小狮子666
2024-06-28*s6.66
2024-06-29*炼1
2024-06-30*!1
2024-07-08*方20
2024-07-18A*16.66
2024-07-31*北12
2024-08-13*基1
2024-08-23n*s2
2024-09-02*源50
2024-09-04*J2
2024-09-06*强8.8
2024-09-09*波1
2024-09-10*口1
2024-09-10*波1
2024-09-12*波10
2024-09-18*明1.68
2024-09-26B*h10
2024-09-3010
2024-10-02M*i1
2024-10-14*朋10
2024-10-22*海10
2024-10-23*南10
2024-10-26*节6.66
2024-10-27*o5
2024-10-28W*F6.66
2024-10-29R*n6.66
2024-11-02*球6
2024-11-021*鑫6.66
2024-11-25*沙5
2024-11-29C*n2.88
posted @   大奥特曼打小怪兽  阅读(4400)  评论(0编辑  收藏  举报
编辑推荐:
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
如果有任何技术小问题,欢迎大家交流沟通,共同进步

公告 & 打赏

>>

欢迎打赏支持我 ^_^

最新公告

程序项目代做,有需求私信(小程序、网站、爬虫、电路板设计、驱动、应用程序开发、毕设疑难问题处理等)。

了解更多

点击右上角即可分享
微信分享提示