Zeros Tech Zeros Tech
首页
架构
大数据
数据库
  • 面试

    • Java面试
    • 大数据面试
    • 架构面试
语言
运维
关于
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

迹_Jason

全栈工程师
首页
架构
大数据
数据库
  • 面试

    • Java面试
    • 大数据面试
    • 架构面试
语言
运维
关于
  • 网站
  • 资源
  • Vue资源
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 技术选型

  • SpringCloud

  • 单元测试

    • Spock
      • 资源
  • 微服务

  • 架构
  • 单元测试
迹_Jason
2021-12-17

Spock

# 资源

  • 示例:https://github.com/spockframework/spock-example
  • 官方文档:https://spockframework.org/spock/docs/2.0/index.html
  • (推荐)美团经验文档:https://tech.meituan.com/2021/08/06/spock-practice-in-meituan.html
class MyFirstSpecification extends Specification {
  // fields
  // fixture methods
  // feature methods
  // helper methods
}

#共享方法,可以被多个功能方法使用
@Shared res = new VeryExpensiveResource()

def setupSpec() {}    // runs once -  before the first feature method
def setup() {}        // runs before every feature method
def cleanup() {}      // runs after every feature method
def cleanupSpec() {}  // runs once -  after the last feature method

// where 
// where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:
// 输入参数1 | 输入参数2 || 输出结果1 | 输出结果2
// 即把请求参数值和返回结果值的字符串动态替换掉,#id、#postCodeResult、#abbreviationResult#号后面的变量是在方法内部定义的,实现占位符的功能。
// @Unroll注解,可以把每一次调用作为一个单独的测试用例运行,这样运行后的单元测试结果更加直观:
@Unroll
def "input 学生id:#id, 返回的邮编:#postCodeResult, 返回的省份简称:#abbreviationResult"() {
  given: "Mock返回的学生信息"
  studentDao.getStudentInfo() >> students

  when: "获取学生信息"
  def response = tester.getStudentById(id)

  then: "验证返回结果"
  with(response) {
    postCode == postCodeResult
    abbreviation == abbreviationResult
  }
  where: "经典之处:表格方式验证学生信息的分支场景"
  id | students                    || postCodeResult | abbreviationResult
  1  | getStudent(1, "张三", "北京") || "100000"       | "京"
  2  | getStudent(2, "李四", "上海") || "200000"       | "沪"
}

def getStudent(def id, def name, def province) {
  return [new StudentDTO(id: id, name: name, province: province)]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

异常处理

    @Unroll
    def "validate student info: #expectedMessage"() {
        when: "校验"
        tester.validateStudent(student)

        then: "验证"
        def exception = thrown(expectedException)
        exception.code == expectedCode
        exception.message == expectedMessage

        where: "测试数据"
        student           || expectedException | expectedCode | expectedMessage
        getStudent(10001) || BusinessException | "10001"      | "student is null"
        getStudent(10002) || BusinessException | "10002"      | "student name is null"
        getStudent(10003) || BusinessException | "10003"      | "student age is null"
        getStudent(10004) || BusinessException | "10004"      | "student telephone is null"
        getStudent(10005) || BusinessException | "10005"      | "student sex is null"
    }

    def getStudent(code) {
        def student = new StudentVO()
        def condition1 = {
            student.name = "张三"
        }
        def condition2 = {
            student.age = 20
        }
        def condition3 = {
            student.telephone = "12345678901"
        }
        def condition4 = {
            student.sex = "男"
        }

        switch (code) {
            case 10001:
                student = null
                break
            case 10002:
                student = new StudentVO()
                break
            case 10003:
                condition1()
                break
            case 10004:
                condition1()
                condition2()
                break
            case 10005:
                condition1()
                condition2()
                condition3()
                break
        }
        return student
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

Mock

class StudentServiceSpec extends Specification {
    def studentDao = Mock(StudentDao)
    def tester = new StudentService(studentDao: studentDao)

    def "test getStudentById"() {
        given: "设置请求参数"
        def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
        def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")

        and: "mock studentDao返回值"
        studentDao.getStudentInfo() >> [student1, student2]

        when: "获取学生信息"
        def response = tester.getStudentById(1)

        then: "结果验证"
        with(response) {
            id == 1
            abbreviation == "京"
            postCode == "100000"
        }
    }
}
// def studentDao = Mock(StudentDao) 这一行代码使用Spock自带的Mock方法,构造一个studentDao的Mock对象,如果要模拟studentDao方法的返回,只需studentDao.方法名() >> "模拟值"的方式,两个右箭头的方式即可

// _ 表示匹配任意类型参数
List<StudentDTO> students = studentDao.getStudentInfo(_);

// 如果有同名的方法,使用as指定参数类型区分
List<StudentDTO> students = studentDao.getStudentInfo(_ as String);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

静态方法Mock

使用 PowerMockito 与 Spock 结合。

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(Sputnik.class)
@PrepareForTest([AbbreviationProvinceUtil.class])
@SuppressStaticInitializationFor(["example.com.AbbreviationProvinceUtil"])
class StudentServiceStaticSpec extends Specification {
    def studentDao = Mock(StudentDao)
    def tester = new StudentService(studentDao: studentDao)

    void setup() {
        // mock静态类
        PowerMockito.mockStatic(AbbreviationProvinceUtil.class)
    }

    def "test getStudentByIdStatic"() {
        given: "创建对象"
        def student1 = new StudentDTO(id: 1, name: "张三", province: "北京")
        def student2 = new StudentDTO(id: 2, name: "李四", province: "上海")

        and: "Mock掉接口返回的学生信息"
        studentDao.getStudentInfo() >> [student1, student2]

        and: "Mock静态方法返回值"
        PowerMockito.when(AbbreviationProvinceUtil.convert2Abbreviation(Mockito.any())).thenReturn(abbreviationResult)

        when: "调用获取学生信息方法"
        def response = tester.getStudentByIdStatic(id)

        then: "验证返回结果是否符合预期值"
        with(response) {
            abbreviation == abbreviationResult
        }
        where:
        id || abbreviationResult
        1  || "京"
        2  || "沪"
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

Mock StructMap

@Mapper
public interface OrderMapper {
    // 即使不用static final修饰,接口里的变量默认也是静态、final的
    static final OrderMapper INSTANCE = Mappers.getMapper(OrderMapper.class);

    @Mappings({})
    OrderVO convert(OrderDTO requestDTO);
}
//===================
@Unroll
def "test convertOrders"() {
  given: "Mock掉OrderMapper的静态final变量INSTANCE,并结合Spock设置动态返回值"
  def orderMapper = Mock(OrderMapper.class)
  Whitebox.setInternalState(OrderMapper.class, "INSTANCE", orderMapper)
  orderMapper.convert(_) >> order

  when: 
  def orders = service.convertOrders([new OrderDTO()])

  then: "验证结果"
  with(orders) {
    it[0].orderDesc == desc
  }

  where: "测试数据"
  order                || desc
  new OrderVO(type: 1) || "App端订单"
  new OrderVO(type: 2) || "H5端订单"
  new OrderVO(type: 3) || "PC端订单"
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Dao层测试

<dependency>
     <groupId>com.h2database</groupId>
     <artifactId>h2</artifactId>
     <version>1.4.200</version>
     <scope>test</scope>
 </dependency>
 <dependency>
     <groupId>org.dbunit</groupId>
     <artifactId>dbunit</artifactId>
     <version>2.5.1</version>
     <scope>test</scope>
 </dependency>
 
 <!-- 测试插件 -->
 <!-- 增加Groovy的maven插件、资源文件拷贝以及测试覆盖率统计插件。 -->
<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>1.8.1</version>
  <executions>
    <execution>
      <goals>
        <goal>addSources</goal>
        <goal>addTestSources</goal>
        <goal>generateStubs</goal>
        <goal>compile</goal>
        <goal>generateTestStubs</goal>
        <goal>compileTests</goal>
        <goal>removeStubs</goal>
        <goal>removeTestStubs</goal>
      </goals>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.0.0-M3</version>
  <configuration>
    <useFile>false</useFile>
    <includes>
      <include>**/*Spec.java</include>
    </includes>
    <parallel>methods</parallel>
    <threadCount>10</threadCount>
    <testFailureIgnore>true</testFailureIgnore>
  </configuration>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-resources-plugin</artifactId>
  <version>2.6</version>
  <executions>
    <execution>
      <id>copy-resources</id>
      <phase>compile</phase>
      <goals>
        <goal>copy-resources</goal>
      </goals>
      <configuration>
        <outputDirectory>${basedir}/target/resources</outputDirectory>
        <resources>
          <resource>
            <directory>${basedir}/src/main/resources</directory>
            <filtering>true</filtering>
          </resource>
        </resources>
      </configuration>
    </execution>
  </executions>
</plugin>
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.2</version>
  <executions>
    <execution>
      <id>prepare-agent</id>
      <goals>
        <goal>prepare-agent</goal>
      </goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>prepare-package</phase>
      <goals>
        <goal>report</goal>
      </goals>
    </execution>
    <execution>
      <id>post-unit-test</id>
      <phase>test</phase>
      <goals>
        <goal>report</goal>
      </goals>
      <configuration>
        <dataFile>target/jacoco.exec</dataFile>
        <outputDirectory>target/jacoco-ut</outputDirectory>
      </configuration>
    </execution>
  </executions>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
class Demo1Spec extends MyBaseSpec {

    /**
     * 直接获取待测试的mapper
     */
    def personInfoMapper = MapperUtil.getMapper(PersonInfoMapper.class)

    /**
     * 测试数据准备,通常为sql表结构创建用的ddl,支持多个文件以逗号分隔。
     */
    def setup() {
        executeSqlScriptFile("com/xxx/xxx/xxx/......../schema.sql")
    }
    /**
     * 数据表清除,通常待drop的数据表
     */
    def cleanup() {
        dropTables("person_info")
    }

    /**
     * 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
     */
    @MyDbUnit(
            content = {
                person_info(id: 1, name: "abc", age: 21)
                person_info(id: 2, name: "bcd", age: 22)
                person_info(id: 3, name: "cde", age: 23)
            }
    )
    def "demo1_01"() {
        when:
        int beforeCount = personInfoMapper.count()
        // groovy sql用于快速执行sql,不仅能验证数据结果,也可向数据中添加数据。
        def result = new Sql(dataSource).firstRow("select * from `person_info`") 
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 3
        result.name == "abc"
        deleteCount == 1
        afterCount == 2
    }

    /**
     * 直接构造数据库中的数据表,此方法适用于数据量较小的mapper sql测试
     */
    @MyDbUnit(content = {
        person_info(id: 1, name: 'a', age: 21)
    })
    def "demo1_02"() {
        when:
        int beforeCount = personInfoMapper.count()
        def result = new Sql(dataSource).firstRow("select * from `person_info`")
        int deleteCount = personInfoMapper.deleteById(1L)
        int afterCount = personInfoMapper.count()

        then:
        beforeCount == 1
        result.name == "a"
        deleteCount == 1
        afterCount == 0
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
编辑 (opens new window)
上次更新: 2021/12/17, 16:15:07
权限
分布式事务

← 权限 分布式事务→

最近更新
01
权限
12-17
02
SpringGateway
12-17
03
分布式事务
12-17
更多文章>
Theme by Vdoing | Copyright © 2021-2021 迹_Jason | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×