Springboot 单元测试

1.Springboot的测试类库
SpringBoot 提供了许多实用工具和注解来帮助测试应用程序,主要包括以下两个模块。

spring-boot-test: 支持测试的核心内容。
spring-boot-test-autoconfigure:支持测试的自动化配置。

开发进行只要引入spring-boot-starter-test的依赖 就能引入这些SpringBoot测试模块,还能引入一些像Junit,AssertJ,Hamcrest及其他一些有用的类库,具体如下所示。

Junit: Java应用程序单元测试标准类库。
Spring Test & Spring Boot Test: Spring Boot 应用程序功能集成化测试支持。
AssertJ: 一个轻量级断言类库。
Hamcrest: 一个对象匹配器类库。
Mockito: 一个java Mock测试框架。
JSONassert: 一个用于JSON的断言库。
JsonPath: 一个Json操作类库。

Maven 依赖

<dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>//在测试环境中生效
 </dependency>

2.创建测试类和测试方法
要让一个普通类变成一个单元测试类:

1.在类名上加入@SpringBootTest 和@RunWith(SpringRunner.class)两个注解即可。
2.在测试方法上加上@Test注解。

使用IDEA可以使用快捷键ctrl + shift + t 或者选中当前类名 使用快捷键alt + enter ,向下选择Create Test 即可进入测试类的选项中,再次回车,就快速的生成测试类。
在这里插入图片描述
生成的测试类在src/test目录下,测试类和源代码包名是一致的(每一个方法对应一个测试方法)。
在这里插入图片描述
Spring Boot测试
步骤一:

当测试中需要使用Spring Boot功能时,可以使用@SpringBootTest注解,装配Spring上下文。
当作为普通Spring单元测试时,可以使用@ContextConfiguration,装配Spring 上下文。

步骤二:

基于Spring的Junit测试单元需要使用@RunWith(SpringJUnit4ClassRunner.class)注解,该注解能够让Junit运行在Spring测试环境中,得到Spring上下文支持。在4.3版本中提供了等同于SpringJUnit4ClassRunner.class的简写类SpringRunner.class

Spring Boot测试单元启动流程如下:

如果未指定classes 参数或者指定的classes参数不是启动main函数入口SpringBootTest(classes = SpringTestAutoConfig.class),则会自动从当前测试类包一层一层向上检索,直到找到@SpringBootApplication@SpringBootConfiguration注释类为止。以此来启动Spring Boot应用,并装载Spring上下文。

如果未检索到Spring Boot启动主配置类,则会抛出异常: java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=…) with your test

如果制定的classes为普通Configer(@SpringBootConfiguration)配置类,则会以此配置初始化Spring 上下文,而不会加载其他Bean到Spring容器。可以在Junit测试单元中使用这些类。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {YourApplication.class})
public class BeanInjectTest {
    // ..
}

3.JUnit4测试
JUnit4中的注解

@BeforeClass和@AfterClass在类被实例化前(构造方法执行前)就被调用了,而且只执行一次,通常用来初始化和关闭资源。
@Before和@After和在每个@Test执行前后都会被执行一次。
@Test标记一个方法为测试方法单元,被@Ignore标记的测试方法不会被执行。

JUnit4为了保证每个测试方法都是单元测试,是独立的互不影响。所以每个测试方法执行前都会重新实例化测试类。

1.超时测试
如果一个测试用例比起指定的毫秒数要花费更多时间,那么JUnit将自动将他标记为失败,timeout参数和@test注解一起使用。现在让我们看看活动中的@Test(timeout)

@Test(timeout = 1000)
public void testTimeout() throws InterruptedException {
    TimeUnit.SECONDS.sleep(2);
    System.out.println("Complete");
}

上面测试会失败,在一秒后会抛出异常org.junit.runners.model.TestTimeOutException:test timedout after 1000 millseconds

2.异常测试
你可以测试代码是否它抛出想要得到的异常。expected参数和@Test 注释一起使用。现在让我们看看活动中的@Test(expected)

3.套件测试
测试套件意味着捆绑几个单元测试用例并且一起执行他们。在 JUnit 中,@RunWith 和 @Suite 注释用来运行套件测试。这个教程将向您展示一个例子,其中含有两个测试样例 TestJunit1 & TestJunit2 类,我们将使用测试套件一起运行他们。

创建一个类
在目录 C:\ > JUNIT_WORKSPACE 中创建一个被测试的 java 类命名为 MessageUtil.java

/*
* This class prints the given message on console.
*/
public class MessageUtil {

   private String message;

   //Constructor
   //@param message to be printed
   public MessageUtil(String message){
      this.message = message; 
   }

   // prints the message
   public String printMessage(){
      System.out.println(message);
      return message;
   }   

   // add "Hi!" to the message
   public String salutationMessage(){
      message = "Hi!" + message;
      System.out.println(message);
      return message;
   }   
}  

创建 Test Case 类
在目录 C:\ > JUNIT_WORKSPACE 创建一个 java 测试类叫做 TestJunit1.java。

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

public class TestJunit1 {

   String message = "Robert";   
   MessageUtil messageUtil = new MessageUtil(message);

   @Test
   public void testPrintMessage() { 
      System.out.println("Inside testPrintMessage()");    
      assertEquals(message, messageUtil.printMessage());     
   }
}

在目录 C:\ > JUNIT_WORKSPACE 创建一个 java 测试类叫做 TestJunit2.java。

import org.junit.Test;
import org.junit.Ignore;
import static org.junit.Assert.assertEquals;

public class TestJunit2 {

   String message = "Robert";   
   MessageUtil messageUtil = new MessageUtil(message);

   @Test
   public void testSalutationMessage() {
      System.out.println("Inside testSalutationMessage()");
      message = "Hi!" + "Robert";
      assertEquals(message,messageUtil.salutationMessage());
   }
}

使用 Test Suite 类
创建一个 java 类。
在类中附上 @RunWith(Suite.class) 注释。
使用 @Suite.SuiteClasses 注释给 JUnit 测试类加上引用。
在目录 C:\ > JUNIT_WORKSPACE 创建一个 java 类文件叫做 TestSuite.java 来执行测试用例。

import org.junit.runner.RunWith;
import org.junit.runners.Suite;
@RunWith(Suite.class)
@Suite.SuiteClasses({
   TestJunit1.class,
   TestJunit2.class
})
public class JunitTestSuite {   
}  

创建 Test Runner 类
在目录 C:\ > JUNIT_WORKSPACE 创建一个 java 类文件叫做 TestRunner.java 来执行测试用例。

import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;

public class TestRunner {
   public static void main(String[] args) {
      Result result = JUnitCore.runClasses(JunitTestSuite.class);
      for (Failure failure : result.getFailures()) {
         System.out.println(failure.toString());
      }
      System.out.println(result.wasSuccessful());
   }
}  

使用 javac 命令编译所有的 java 类

C:\JUNIT_WORKSPACE>javac MessageUtil.java TestJunit1.java 
TestJunit2.java JunitTestSuite.java TestRunner.java

现在运行 Test Runner,即运行所有的在之前 Test Case 类中定义的测试用例。

C:\JUNIT_WORKSPACE>java TestRunner
验证输出

Inside testPrintMessage()
Robert
Inside testSalutationMessage()
Hi Robert
true

4.参数化测试
JUnit4 引入了一个新的功能参数化测试。参数化测试允许开发人员使用不同的值反复运行同一个测试。
创建参数化测试,遵循以下5个步骤。

RunWith(Parameterized.class)来注释test类。
创建一个由@Parameters注释的公共的静态方法,它返回一个对象的集合(数组)来作为数据集合。
创建一个公共的构造函数,它接受数据集合相同的参数。
为每一列测试数据创建一个实例变量。
用实例变量作为测试数据的来源来创建你的测试用例。

/*更改默认的测试运行器为RunWith(Parameterized.class)*/
@RunWith(Parameterized.class)
public class ParameterTest {

    /*声明变量存放预期值和测试数据*/
    private String firstName;
    private String lastName;

    /*声明一个返回值 为Collection的公共静态方法,并使用@Parameters进行修饰*/
    @Parameterized.Parameters
    public static List<Object[]> param() {
        /*这里给出两个测试用例*/
        return Arrays.asList(new Object[][]{{"Mike","Black"},{"Circle","Smith"}});
    }

    public ParameterTest (String firstName,String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    /*进行测试,发现它会将所有的测试用例测试一遍*/
    @Test
    public void test(){
        String name = firstName + " " + lastName;
        System.out.println(name);
    }

4.Assert

assert常用方法

assertEquals(“message”,A,B):判断对象A和B是否相等,这个判断比较时调用了equals()方法。
assertSame(“message”,A,B):判断对象A和B是否相同,使用的是==操作符。
assertTure(“message”,A):判断A条件是否为真。
assertFalse(“message”,A):判断A条件是否不为真。
assertNotNull(“message”,A):判断A对象是否不为null
assertArrayEquals(“message”,A,B): 判断A数组与B数组是否相等。

5.Mockito

什么是mock
在面向对象的程序设计中,模拟对象(mock object)是以可控的方式模拟真实对象行为的假对象。在编程过程中,通常通过模拟一些输入数据,来验证程序是否达到预期结果。

为什么使用Mock对象
使用模拟对象,可以模拟复杂的、真实的对象行为。如果在单元测试中无法使用真实对象,可采用模拟对象进行替代。

在以下情况可以采用模拟对象来替代真实对象:

真实对象的行为是不确定的(例如,当前的时间或温度)。
真实对象很难搭建起来。
真实对象的行为很难触发(例如,网络错误)。
真实对象速度很慢。
真实对象是用户界面,或包括用户界面在内。
真实的对象使用了回调机制。 真实对象可能还不存在。
真实对象可能包含不能用作测试的信息和方法。

使用Mockito一般分为三个步骤:

1.模拟测试类所需的外部依赖
2.执行测试代码
3.判断执行结果是否达到预期

Mockito
JUnit和SpringTest基本上可以满足绝大多数单元测试,但是由于现在系统越来越复杂,相互之间依赖越来越多。特别是微服务化以后的系统,往往一个模块的代码需要依赖几个其他模块的东西。因此,在做单元测试的时候,往往很难构造出需要的依赖。一个单元测试,我们只关心一个小的功能,但是为了这个小的功能能跑起来,可能需要依赖一堆其他的东西,这就导致了单元测试无法进行。所以我们就需要在测试过程中引入mock测试。

所谓的Mock测试就是在测试过程中,对于一些不容易构造的、或者和这次单元测试无关但是上下文又有依赖的对象,用一个虚拟的对象(Mock对象)来模拟,以便单元测试能够进行。
比如有一段代码的依赖为:
在这里插入图片描述
当我们要进行单元测试的时候,就需要给A注入B和C但是C又依赖了D,D又依赖了E。这就导致了A的单元测试很难进行。

但是当我们使用Mock来进行模拟对象后,我们就可以把这种依赖解耦,只关心A本身的测试,它所依赖的B和C,全部使用Mock出来的对象,并且给MockBMockC指定一个明确的行为。就像这样:

在这里插入图片描述
因此,当我们使用Mock后,对于那些难以构建的对象,就变成了个模拟对象,只需要提前的做Stubbing(桩)即可。所谓的做桩数据,也就是告诉Mock对象,当与之交互时执行何种行为过程。比如当调用B对象的b()方法时,我们期望返回一个true,这就是一个设置桩数据的预期。

mockito 使用详解
现有如下代码:

实体类

@Entity
@Data
@NoArgsConstructor
public class User implements Serializable{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true,nullable = false,length = 50)
    private String username;

    private String password;

    @CreationTimestamp
    private Date createDate;

    public User(Long id,String username) {
        this.id = id;
        this.username = username;
    }
}

Repository

public interface IUserRepository extends JpaRepository<User,Long>{
    boolean updateUser(User user);
}

Service

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements IUserService {
    private final IUserRepository userRepository;


    @Override
    public User findOne(Long id) {
        return userRepository.getOne(id);
    }

    @Override
    public boolean updateUsername(Long id, String username) {
        User user = findOne(id);
        if(user == null) {
            return false;
        }
        user.setUsername(username);
        return userRepository.updateUser(user);
    }
}

Test

public class IUserServiceTest {
    private IUserService userService;

//    @Mock
    private IUserRepository userRepository;


    @Before
    public void setUp() throws Exception {
        /*对所有注解了@Mock的对象进行模拟*/
//        MockitoAnnotations.initMocks(this);
        /*如果不使用注解,可以对单个对象进行mock*/
        userRepository = Mockito.mock(IUserRepository.class);
        /*构造测试对象*/
        userService = new UserServiceImpl(userRepository);
        /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果User*/
        Mockito.when(userRepository.getOne(1L)).thenReturn(new User(1L,"jack"));
         /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置返回的结果null*/
        Mockito.when(userRepository.getOne(2L)).thenReturn(null);
         /*打桩,构建当userRepository getOne函数执行参数为1的时候,设置抛出异常*/
        Mockito.when(userRepository.getOne(3L)).thenThrow(new IllegalArgumentException("the id is not support"));
         /*打桩,构建当userRepository updateUser执行任意User类型的参数,返回的结果都是true*/
        Mockito.when(userRepository.updateUser(Mockito.any(User.class))).thenReturn(true);
        /*打桩,给void方法 */
        Mockito.doAnswer(invocation -> {
            System.out.println("进入Mock");
        return null;
        }).when(userRepository).addUser(Mockito.any());

        /*模拟方法设置返回期望值*/
        List spy = Mockito.spy(new LinkedList<>());
        /*这里会抛出IndexOutOfBoundsException*/
//        Mockito.when(spy.get(0)).thenReturn("foo");
        /*所以要使用下面代码*/
        Mockito.doReturn("foo").when(spy).get(0);
    }



    @Test
    public void testUpdateUsernameSuccess() throws Exception {
        Long userId = 1L;
        String newUsername = "new Jack";
        /*测试service方法*/
        boolean updated = userService.updateUsername(userId,newUsername);
        /*检查结果*/
        Assert.assertThat(updated, Matchers.is(true));

        /*Mock对象一旦创建,就会自动记录自己的交互行为。通过verify(mock).someMethod()方法,来验证方法是否被调用。*/
        /*验证调用上面的service方法后是否 userRepositroy.getOne(1L)调用过。*/
        Mockito.verify(userRepository).getOne(userId);

        /*updateUsername 函数中我们调用了已经打桩了的其他的函数,现在我们来验证进入其他函数中的参数*/
        /*构造参数捕获器,用于捕获方法参数进行验证*/
        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        /*验证updateUser方法是否呗调用过,并且捕获入参*/
        Mockito.verify(userRepository).updateUser(userCaptor.capture());
        /*获取参数updateUser*/
        User updateUser = userCaptor.getValue();
        /*验证入参是否是预期的*/
        Assert.assertThat(updateUser.getUsername(),Matchers.is(newUsername));
        /*保证这个测试用例中所有被Mock的对象的相关方法都已经被Verify过了*/
        Mockito.verifyNoMoreInteractions(userRepository);
        /*如果有一个交互没有被verify,则会报错
        org.mockito.exceptions.verification.NoInteractionsWanted:
        No interactions wanted here:
        -> at com.wuwii.service.IUserServiceTest.testUpdateUsernameSuccess(IUserServiceTest.java:74)
        But found this interaction on mock 'iUserRepository':
        -> at com.wuwii.service.impl.UserServiceImpl.findOne(UserServiceImpl.java:21)
        ****/

    }

//    @Test
    public void testUpdateUsernameFailed() throws Exception {
        Long userId = 2L;
        String newUsername = "new Jack";
        /*没有经过mock的updateUser方法,它的返回值是false*/
        boolean updated = userService.updateUsername(userId,newUsername);
        Assert.assertThat(updated,Matchers.is(true));
        /*验证userRepository的getOne(2L)这个方法是否被调用过(这个是被测试过的,此步骤通过)*/
        Mockito.verify(userRepository).getOne(2L);
        /*验证userRepository的updateUser(null)这个方法是否被调用过(这个方法是没有被调用过的)*/
        Mockito.verify(userRepository).updateUser(null);
        Mockito.verifyNoMoreInteractions(userRepository);

    }

创建Mock对象
我们需要对userService进行测试,就需要模拟userRepository对象
我们在setUp()方法中,模拟对象并打桩。

模拟对象有两种方式:

1.对注解@Mock的对象进行模拟MockitoAnnotations.initMocks(this)
2.对单个对象手动Mock: userRepositroy = Mockito.mock(IUserRepositroy.class)

数据打桩
数据打桩,方法非常多,主要分下面几种:

1.最基本的用法就是调用when以及thenReturn方法了。它的作用就是指定当我们调用被代理的对象的某一个方法以及参数的时候,返回什么值。

2.提供参数匹配器,灵活匹配参数。any()any(Class<T> type)anyBoolean()anyByte() anyChar()anyInt()、 anyLong()等等,它支持复杂的过滤,可以使用正则Mockito.matches(".*User$"),开头结尾验证 endsWith(String suffix)、 startsWith(String prefix),判空验证 isNotNull()、 isNull() 。也还可以使用argThat(ArgumentMatchermatcher),如ArgumentMatcher只有一个方法boolean matches(T argument);传入入参,返回一个boolean表示是否匹配。Mockito.argThat(argument ->argument.getUsername.length() > 6)

3.Mockito还提供了了两个表示行为的方法:thenAnswer(Answer<?> answer);、thenCallRealMethod();分别表示自定义处理调用后的行为,以及调用真实的方法。这两个方法在有些测试用例中还是很有用的。

4.对于同一个方法,Mockito可以是顺序与次数关连的。也就是说可以实现同一个方法 ,第一次调用返回一个值,第二次调用返回一个值,甚至第三次调用抛出异常等等。只需要连续的调用thenXXXX即可。

  1. 如果为一个返回Void的方法设置桩数据。上面的方法都是表示的是有返回值的方法,而由于一个方法没有返回值,因此我们不能调用when方法,比如:doAnswer(Answer answer)、doNothing()、doReturn(Object toBeReturned)、doThrow(Class<? extends Throwable> toBeThrown)、doCallRealMethod()。它们使用方法其实和上面thenXXXX是一样的.

验证测试方法的结果

  1. 使用断言来检查结果。
  2. 验证Mock对象的调用
    其实,在这里我们如果只是验证方法结果的正确的话,就非常简单,但是在复杂的方法调用堆栈中,往往可能出现结果正确,但是过程不正确的情况。比如updateUsername方法返回false有两种可能,一直可能是用户没有找到,还有一种可能就是userRepository.updateUser(userPO)返回false。因此如果我们只使用Assert.assertFalse(updated);来验证结果,可能就会忽略某些错误。

因此我们在测试中还需要验证指定的方法userRepository.getOne(userId);是否运行过,而且我们还是用了参数捕获器,抓取中间的方法参数。用来验证。

提供了verify(T mock,VerificationMode mode)方法。VerificationMode有很多作用。

/*验证指定方法 get(3) 没有被调用*/
verify(mock,never()).get(3);

verifyZeroInteractions和verifyNoMoreInteractions验证所有mock的方法是否都调用过了。

Spring MVC 测试 --MockMvc
如果要对Spring MVC 测试,可以使用@WebMvcTest 注解,可以在不需要完整启动HTTP服务器就可以快速测试MVC控制器。
使用@WebMvcTest注解时需要注意,只有部分bean会被加载到Spring 上下文中,分别是:

@Controller
@ControllerAdvice
@JsonComponent
Filter
WebMvcConfigurer
HandlerMethodArgumentResolver

其他常规的@Component @Bean @Service注解的Bean并不会加载到Spring测试环境。如果要使用这些类,需要使用mock打桩的方式。

import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
    
@RunWith(SpringRunner.class)
@WebMvcTest(HelloController.class)
public class HelloControllerTest {
    
    @Autowired
    private MockMvc mvc;
    
    @MockBean
    private HelloService helloService;
    
    @Test
    public void testSayHi() throws Exception {
        // 模拟 HelloService.sayHi() 调用, 返回 "=== Hi ==="
        when(helloService.sayHi()).thenReturn("=== Hi ===");
        mvc.perform(get("/hello/sayHi"))
                .andExpect(status().isOk())
                .andDo(print());
    }
    
}

MockMvc是由spring-test包提供,实现了对Http请求的模拟,能够直接使用网络的形势,转换到Controller的调用,是的测试速度快,不依赖网络环境。同时提供了一套验证的工具,结果的验证十分方便。
接口MockMvcBuilder,一共一个唯一的build方法,用来构造MockMvc。主要有两个实现:StandaloneMockMvcBuilderDefaultMockMvcBuilder,分别对应两种测试方式,即独立安装和继承web环境测试(并不会集成真正的web环境,而是通过相应的Mock API进行模拟测试,无需启动服务)。MockMvcBuilders提供了对应的创建方法standaloneSetup 方法和webAppContextSetup方法,在使用时直接调用即可。

private MockMvc mockMvc;

@Autowire
private WebApplicationContext webApplicationContext;

@Before
public void setup() {
    /*实例化方式一*/
    mockMvc = MockMvcBuilders.standaloneSetup(new UserController()).build();
    /*实例化方式二*/
    mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

单元测试方法:

@Test
    public void testHello() throws Exception {
/*
        1.mockMvc.perform 执行一个请求
        2.MockMvcRequestBuilders.get("XXX")构造一个请求
        3.ResultActions.param()
        4.ResultActions.accept()
        5.ResultActions.andExpect
        6.ResultActions.andDo 添加一个结果处理器,表示要对结果做点什么事情。
        7.ResultActions.andReturn 表示执行完成后,返回响应的结果。
*/
        mockMvc.perform(MockMvcRequestBuilders.get("/mock-mvc/test-get")
                /*设置返回类型为utf-8,否则默认为ISO-8859-1*/
                .accept(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .param("name","tom"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().string("hello"))
                .andDo(MockMvcResultHandlers.print());
    }

整个过程如下

1.准备测试环境
2.通过MockMvc执行请求
3.添加验证断言
4.添加结果处理器
5.得到MvcResult进行分自定义断言/进行下一步异步请求
6.卸载测试环境

注意事项:如果使用DefaultMockMvcBuilder进行MockMvc实例化时需在SpringBoot启动类上添加组件扫描的package的指定,否则会出现404

@ComponentScan(basePackages = "com.creators")

相关API
RequestBuilder提供了一个方法buildRequest(ServletContext servletContext)用于构建MockHttpServletRequest;其中有两个子类MockHttpServletRequestBuilderMockMultipartHttpServletRequestBuilder(文件上传使用)

MockMvcRequestBuilders提供get、post等多种方法用来实例化RequestBuilder。

ResultActions:MockMvc.perform(RequestBuilder requestBuilder)的返回值,提供三种能力:andExpect 添加断言判断结果是否达到预期;andDo,添加结果处理器,比如示例中的打印。andReturn返回验证成功后的MvcResult,用于自定义验证/下一步的异步处理。
一些常用的测试
测试普通控制器

mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}",1))
        /*验证存储模型数据*/
       .andExpect(MockMvcResultMatchers.model().attributeExists("user"))
        /*验证viewName*/
       .andExpect(MockMvcResultMatchers.view().name("user/view"))
        /*验证视图渲染时forward到的jsp*/
       .andExpect(MockMvcResultMatchers.forwardedUrl("/WEB-INF/jsp/user/view/jsp"))
        /*验证状态码*/
       .andExpect(MockMvcResultMatchers.status().isOk())
        /*输出MvcResult到控制台*/
       .andDo(MockMvcResultHandlers.print());

得到MvcResult自定义验证

 MvcResult result = mockMvc.perform(MockMvcRequestBuilders.get( "/user/{id}",1)) 
        .andReturn();
  Assert.assertNotNull(result.getModelAndView().getModel().get("user"));

验证请求参数绑定到模型数据及flash属性

mockMvc.perform(MockMvcRequestBuilders.post("/user").param("name","wang"))
                /*验证执行控制器类*/
                .andExpect(MockMvcResultMatchers.handler().handlerType(UserController.class))
                /*验证执行控制器方法名*/
                .andExpect(MockMvcResultMatchers.handler().methodName("create"))
                /*验证页面没有错误*/
                .andExpect(MockMvcResultMatchers.model().hasNoErrors())
                /*验证存在flash属性*/
                .andExpect(MockMvcResultMatchers.flash().attributeExists("success"))
                /*验证视图名称*/
                .andExpect(MockMvcResultMatchers.view().name("redirect:/user"));

文件上传

 byte[] bytes = new byte[]{1,2};
        mockMvc.perform(MockMvcRequestBuilders.multipart("/user/{id}/icon",1L).file("icon",bytes))
                .andExpect(MockMvcResultMatchers.model().attribute("icon",bytes))
                .andExpect(MockMvcResultMatchers.view().name("success"));

JSON请求/响应验证

String requestBody = "{\"id\":1,\"name\":\"wang\"}";
        mockMvc.perform(MockMvcRequestBuilders.post("/user")
                .contentType(MediaType.APPLICATION_JSON_UTF8_VALUE)
                .content(requestBody)
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
            /*检查返回JSON数据中某个值的内容: 请参考http://goessner.net/articles/JsonPath/*/
            .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));

        String errorBody = "{id:1,name:wang}";
        MvcResult mvcResult = mockMvc.perform(MockMvcRequestBuilders.post("/user")
                .contentType(MediaType.APPLICATION_JSON).content(errorBody)
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isBadRequest())
                .andReturn();
        Assert.assertTrue(HttpMessageNotReadableException.class.isAssignableFrom(mvcResult.getResolvedException().getClass()));

异步测试

MvcResult mvcResult1 = mockMvc.perform(MockMvcRequestBuilders.get("/user/async?id=1&name=wang"))
                .andExpect(MockMvcResultMatchers.request().asyncStarted())
                .andExpect(MockMvcResultMatchers.request().asyncResult(CoreMatchers.instanceOf(User.class)))
                .andReturn();
        mockMvc.perform(MockMvcRequestBuilders.asyncDispatch(mvcResult1))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.jsonPath("$.id").value(1));

使用MultiValueMap构建参数

MultiValueMap<String,String> params = new LinkedMultiValueMap<>();
        params.add("name","wang");
        params.add("hobby","sleep");
        params.add("hobby","eat");
        mockMvc.perform(MockMvcRequestBuilders.post("/user").params(params));

模拟session和cookie

 mockMvc.perform(MockMvcRequestBuilders.get("/index").sessionAttr("name", "value"));
        mockMvc.perform(MockMvcRequestBuilders.get("/index").cookie(new Cookie("name", "value")))

全局配置

mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext)
                .defaultRequest(MockMvcRequestBuilders.get("/user/1").requestAttr("default",true))
                .alwaysDo(MockMvcResultHandlers.print())
                .alwaysExpect(MockMvcResultMatchers.request().attribute("default",true))
                .build();

如果是测试Service层代码 可以在单元测试方法上加上@Transactional注解,在测试完毕后,数据能自动回滚。

Spring Boot Web测试

开启Spring Http服务对Web应用测试,可以使用注解@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)注解开启一个随机的可用端口。Spring Boot针对REST接口调用提供了TestRestTemplate模版。他可以解析链接服务器的接口地址。

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class ApplicationTest {
    
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    public void testSayHello() {
        Map<String, Object> result = restTemplate.getForObject("/hello/sayHello", Map.class);
        System.out.println(result.get("message"));
    }
    
}

Spring Data JPA测试

对Spring Data JPA进行测试可以使用@DataJpaTest注解。@DataJpaTest使用的是内存数据库进行测试,你无需配置和启用真实的数据。在pom配置文件中添加依赖即可。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

@DataJpaTest注解只会扫描@Entity 的Bean 和装配 Spring Data JPA 存储库,其他常规@Service @Component @Repository 注解的Bean不会加载到Spring 上下文中。

@RunWith(SpringRunner.class)
@DataJpaTest
public class UserRepositoryInMemoryTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void testSave() {
        User user = new User();
        user.setName("fanlychie");
        userRepository.save(user);
        System.out.println("====================================");
        System.out.println(userRepository.findAll());
        System.out.println("====================================");
    }
    
}
@Entity(name = "User")
public class User {
    
    @Id
    @GeneratedValue(generator = "uuidGenerator")
    @GenericGenerator(name = "uuidGenerator", strategy = "uuid")
    private String id;
    
    private String name;
    
    // getters and setters
    
}
public interface UserRepository extends JpaRepository<User, String> {
    
}

真实数据库测试

如果需要使用真实的数据库作为测试,需要添加注解@AutoConfigureTestDatabase(replace = Replace.NONE),通知Spring不要替换原有应用的默认数据库。

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class UserRepositoryMySQLTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void testSave() {
        User user = new User();
        user.setName("fanlychie");
        userRepository.save(user);
        System.out.println("====================================");
        System.out.println(userRepository.findAll());
        System.out.println("====================================");
    }
    
}

事务控制

默认情况下,在每个jpa测试执行完成时,都会执行事务回滚。从而防止测试数据污染数据库,如果不希望回滚可以在测试方法上添加注解@Rollback(false)。也可以在方法或者类上添加@Transactional作为全局的事务控制。

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class UserRepositoryMySQLTest {
    
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional(readOnly = true)
    public void testSelect() {
        System.out.println("====================================");
        System.out.println(userRepository.findAll());
        System.out.println("====================================");
    }
    
}
  • 11
    点赞
  • 61
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值