SpringBoot+Spock的熟悉之路(四):用Spock+H2对SpringBoot进行集成测试

前言

本来打算是把Spock的使用写成一篇的,后来发现太长了而且结构比较冗杂,还是拆分出来比较好,而且这样也比较好检索。初次使用,如果有误,请轻喷,并指出问题。我会及时纠正。
要使用Spock,必须要有Junit和Mock的基本概念和使用。建议在使用Spock之前先懂了解一下Junit和Mockitio
上一篇博客:《SpringBoot+Spock的熟悉之路(三):用Spock对SpringBoot进行单元测试》

环境

一定要注意版本的问题!一定要注意版本的问题!一定要注意版本的问题!

Tool Version
Intellij IDEA 2018.3 Ultimate
SpringBoot 2.0.1
Java 1.8
mybatis-spring-boot 2.0.1
Groovy 2.4.6

完整的依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.0.1</version>
    </dependency>
    <!--服务器配置的是Oracle-->
    <dependency>
        <groupId>com.oracle</groupId>
        <artifactId>ojdbc8</artifactId>
        <version>12.2.0.1</version>
    </dependency>
        
    <!---------------Spock必须的------------------->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
	    <groupId>org.spockframework</groupId>
	    <artifactId>spock-core</artifactId>
	    <version>1.3-groovy-2.4</version>
	    <scope>test</scope>
	</dependency>
	<dependency>
	    <groupId>org.spockframework</groupId>
	    <artifactId>spock-spring</artifactId>
	    <version>1.3-RC1-groovy-2.4</version>
	    <scope>test</scope>
	</dependency>
	<dependency>
	    <groupId>org.codehaus.groovy</groupId>
	    <artifactId>groovy-all</artifactId>
	    <version>2.4.6</version>
	</dependency>
	<!--------------  可选项  ----------------->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
	    <groupId>org.projectlombok</groupId>
	    <artifactId>lombok</artifactId>
	    <optional>true</optional>
    </dependency>
    <!----------------方便MockMvc的时候模拟数据--------------->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.51</version>
    </dependency>
     <!----------------H2数据库支持--------------->
    <dependency>
	   <groupId>com.h2database</groupId>
	   <artifactId>h2</artifactId>
	   <version>1.4.197</version>
	</dependency>
</dependencies>

<build>
   <plugins>
       <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
       </plugin>
   </plugins>
   <!-------为了能让项目识别资源文件--------->
   <resources>
       <resource>
           <directory>src/main/resources</directory>
           <includes>
               <include>**/*.properties</include>
               <include>**/*.xml</include>
               <include>**/*.sql</include>
           </includes>
           <filtering>false</filtering>
       </resource>
       <resource>
           <directory>src/main/java</directory>
           <includes>
               <include>**/*.properties</include>
               <include>**/*.xml</include>
               <include>**/*.sql</include>
           </includes>
           <filtering>false</filtering>
       </resource>
   </resources>
</build>

事先准备

添加配置文件

关于集成测试,最好的办法还是在man下的配置文件里建一个application-qa.properties,将集成测试所要用到的数据库连接等信息放进去。大概样子类似于这样
在这里插入图片描述
application.properties文件里的内容如下

server.port= 9090

spring.redis.host=127.0.0.1
spring.redis.port=6379

spring.datasource.url=jdbc:oracle:thin:@//********:***/*****
spring.datasource.username=********
spring.datasource.password=********

# MyBatis配置
mybatis.config-location=classpath:mybatis.xml

application-qa.properties文件里的内容如下

server.port=8080

#************H2  Begin****************
#db schema
#初始化数据库中数据,可以没有
spring.datasource.schema=classpath:db/schema.sql

#remote visit
spring.h2.console.settings.web-allow-others=true
#console url
spring.h2.console.path=/h2-console
#default true
spring.h2.console.enabled=true
spring.h2.console.settings.trace=true

#DB_CLOSE_ON_EXIT=FALSE的意思是,关闭了JVM后,数据不会删除,DATABASE_TO_UPPER的意思是,让数据库对大小写敏感
spring.datasource.url=jdbc:h2:file:C:/data/sample;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=Oracle
#driver default:org.h2.Driver
spring.datasource.driver-class-name=org.h2.Driver
#default sa
spring.datasource.username=sa
#default null
spring.datasource.password=
#************H2  End****************

schema.sql

CREATE TABLE DEMO(
DEMO_ID NUMBER PRIMARY KEY NOT NULL,
DEMO_STR VARCHAR2(100)
);

CREATE TABLE DEMO_DICTONARY(
DICTONARY VARCHAR2(100) PRIMARY KEY NOT NULL
);

INSERT INTO DEMO(DEMO_ID,DEMO_STR) VALUES(1,'str1');

数据准备

BaseController

package com.example.demo.base;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.client.RestTemplate;

public abstract class BaseController {

    @Autowired
    protected RestTemplate restTemplate;

}

BaseService

package com.example.demo.base;

import com.example.demo.repository.BaseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import javax.annotation.PostConstruct;
import java.util.List;

public abstract class BaseService {

    //加一个init方法,模拟我们容器完全启动之前会去数据库里查询字典值等额外数据
    @PostConstruct
    public List<String> getDictionary(){
        return baseRepository.getDictionary();
    }

    @Autowired
    protected RedisTemplate<String, String> redisTemplate;

    @Autowired
    private BaseRepository baseRepository;

}

BaseRepository 和xml

package com.example.demo.repository;

import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface BaseRepository {

    List<String> getDictionary();

}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.repository.BaseRepository">

    <select id="getDictionary" resultType="java.lang.String">
        SELECT
          DICTONARY
        FROM
          DEMO_DICTONARY
    </select>
</mapper>

DemoController

package com.example.demo.controller;


import com.example.demo.base.BaseController;
import com.example.demo.entity.DemoEntity;
import com.example.demo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/demo")
public class DemoController extends BaseController {

    @GetMapping(path = "", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public DemoEntity getDemo(Integer demoId) {
        return demoService.getDemo(demoId);
    }

    @Autowired
    private DemoService demoService;
}

Service 和 ServiceImpl

package com.example.demo.service;

import com.example.demo.entity.DemoEntity;

public interface DemoService {

    DemoEntity getDemo(Integer demoId);

}

package com.example.demo.service.impl;

import com.example.demo.entity.DemoEntity;
import com.example.demo.repository.DemoRepository;
import com.example.demo.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemoServiceImpl extends BaseService implements DemoService {


    @Autowired
    private DemoRepository demoRepository;

    @Override
    public DemoEntity getDemo(Integer demoId) {
        return demoRepository.getDemo(demoId);
    }
}

Dao层及对应的Xml文件

package com.example.demo.repository;

import com.example.demo.entity.DemoEntity;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface DemoRepository {

    DemoEntity getDemo(@Param("demoId") Integer demoId);
    
    Integer createDemo(@Param("param")DemoEntity demoEntity);
}

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.repository.DemoRepository">
    <select id="getDemo" resultType="com.example.demo.entity.DemoEntity">
        SELECT
          DEMO_ID  demoId,
          DEMO_STR  demoStr
        FROM
          DEMO
        WHERE
          DEMO_ID = #{demoId,jdbcType=NUMERIC}
    </select>

    <insert id="createDemo">
        INSERT INTO
        DEMO(
          DEMO_ID,
          DEMO_STR
        )
        VALUES(
          #{param.demoId,jdbcType=NUMERIC},
          #{param.demoStr,jdbcType=VARCHAR}
        )
    </insert>
</mapper>

配置H2数据库的嵌入模式

上一篇博客曾说过关于H2数据库的内存模式。这里简单说一下嵌入式模式。
嵌入式模式有点像office里的Access数据库,将数据库文件保存在本地的,也是只需要添加H2的相关依赖和配置信息就可以正常使用。想要删除数据库只需要要将整个数据库文件删除即可。
我上面的配置文件中指定了数据库文件的位置

spring.datasource.url=jdbc:h2:file:C:/data/sample;DB_CLOSE_ON_EXIT=FALSE;DATABASE_TO_UPPER=false;MODE=Oracle

在启动SpringBoot容器之后,就可以在这个地址下面发现这么一个文件。
在这里插入图片描述
由于其有实际的数据库文件,我们可以用第三方软件(DataGrip等)来连接它,也可以使用H2自带的页面端控制台。
配置文件中有这么一句

spring.h2.console.path=/h2-console

那么只要启动SpringBoot容器后,在页面输入127.0.0.1:端口号/h2-console
就可以看到这么一个页面
在这里插入图片描述
将我们的数据库连接字符串输进去点击Connect后,就能进入功能页面,可以看到我们在schema.sql中创建的表格,也可以在中间的输入框内执行基本的sql语句
在这里插入图片描述

一个简单的例子

对于集成测试,不需要去考虑用Mock或者其他的去解决相关的依赖,其要的就是从上到下所有流程全部走一遍,因此不需要像我上一篇单元测试的博客那样整那么多麻烦的注解出来,直接使用@SpringBootTest注解,如果test项目结构与main中的不完全匹配,可能需要添加@SpringBootTest(classes = DemoApplication.class)

package com.example.demo.Integration


import com.example.demo.service.DemoService
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.ActiveProfiles
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders
import spock.lang.Specification

@SpringBootTest
@ActiveProfiles("qa")
@AutoConfigureMockMvc
class IntegrationSpec extends Specification {

    @Autowired
    MockMvc mockMvc

    @Autowired
    private DemoService demoService

    def "get entity test"() {
        expect:
        demoService.getDemo(1) != null
    }
}

启动一下后查看控制台,发现了在BaseService中使用@PostConstruct注解过的,查询字典值的方法,可以看出来确实启动了整个容器,并且我们可以在代码中随意使用@Autowired,而不用像单测一样到处去Mock和Stub
在这里插入图片描述
如果要模拟网页传过来的数据,目前比较好的办法还是使用MockMvc。
新加一个测试类

def "mvc test"() {
    given:
    def demoId = 1
    when:
    String result = mockMvc.perform(MockMvcRequestBuilders.get("/demo").param("demoId", demoId.toString()))
    .andReturn().getResponse().getContentAsString()
    then:
    print result
    result != null

}

执行后结果如下所示
在这里插入图片描述

遇到的问题

本模块主要是把自己踩过的一些比较大的坑给单独列举出来

The file is locked

在这里插入图片描述
按照官方文档的说法,嵌入模式下的H2是不允许多条连接的。所以看一下是不是main的主类已经启动,或者是测试类和主类用的是同一个数据库连接字符串。或者改为Server模式。(之后看找机会把Server模式怎么搭建给补上)

启动的时候报Table “XXX” already exists; SQL statement

正常情况下嵌入模式的数据库只需要在第一次连接的时候执行初始化脚本即可,看一下自己的
spring.datasource.schema=classpath:db/schema.sql是否存在
如果不想去改配置文件,那么最好在初始化脚本前加上DROP TABLE IF EXISTS tableName

参考资料

Spock in Java 慢慢爱上写单元测试
spock-testing-exceptions-with-data-tables
Spock官方文档
Spock开源GitHub
Difference between Mock / Stub / Spy in Spock test framework
Mocks Aren’t Stubs
@Mock/@InjectMocks for groovy - spock
Difference between Mock / Stub / Spy in Spock test framework
spock-subjects-collaborators-extension
mybatis-spring-boot-autoconfigure
H2官方文档

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