开始spock单测

目录

引入maven依赖

项目实战

Spock测试框架

一、为什么要使用Spock

二、Spock基本概念

三、Spock基本使用

基本标签语句组合形

with() 和 verifyAll()

spock异常处理

spring环境中使用spock

四、Spock数据驱动

数据表(data table)

数据管道(Data Pipes)

多变量的数据管道(Multi-Variable Data Pipes)

五、spock测试桩mock和stub的使用

使用stub测试桩

使用mock测试桩

mock和stub测试桩的对比

六、Spock注解使用

七、实战案例

1、数据驱动

2、stub测试桩

八、注意事项

1、新增ut

九、其他


引入maven依赖

            <!-- spock -->
            <dependency>
                <groupId>org.spockframework</groupId>
                <artifactId>spock-core</artifactId>
                <version>1.0-groovy-2.4</version>
                <scope>test</scope>
            </dependency>

项目实战

package com.youzan.ebiz.salesman.wap.biz.groovy

import com.youzan.ebiz.mall.commons.helper.ResultHelper
import com.youzan.ebiz.salesman.api.push.NsqPushService
import com.youzan.ebiz.salesman.api.relation.RelationApiService
import com.youzan.ebiz.salesman.api.shoppingguide.ShoppingGuideCheckApiService
import com.youzan.ebiz.salesman.wap.api.dto.relation.CustomerRelationSaveWapDTO
import com.youzan.ebiz.salesman.wap.api.dto.resp.StatusDTO
import com.youzan.ebiz.salesman.wap.biz.api.customer.CustomerWapApiServiceImpl
import com.youzan.ebiz.salesman.wap.biz.service.WhitelistService
import com.youzan.ebiz.salesman.wap.biz.service.dependency.shoppingguide.GuideFrontRelationWrapperService
import com.youzan.ebiz.salesman.wap.biz.service.dependency.shoppingguide.ShoppingGuideReadApiWrapperService
import com.youzan.guide.api.service.relation.CustomerRelationWriteApiService
import spock.lang.Specification
import spock.lang.Unroll

/**
 * @author zhangkun* @date 2022/1/18 下午7:10
 * @version 1.0
 */
class CustomerWapApiServiceImplTest extends Specification {

    def customerWapApiServiceImpl = new CustomerWapApiServiceImpl();
    def nsqPushService = Mock(NsqPushService);
    def whitelistService = Mock(WhitelistService);
    def guideFrontRelationWrapperService = Mock(GuideFrontRelationWrapperService);
    def relationApiService = Mock(RelationApiService);
    def customerRelationWriteApiService = Mock(CustomerRelationWriteApiService);
    def shoppingGuideCheckApiService = Mock(ShoppingGuideCheckApiService)
    def shoppingGuideReadApiWrapperService = Mock(ShoppingGuideReadApiWrapperService)

    def setup() {
        customerWapApiServiceImpl.nsqPushService = nsqPushService
        customerWapApiServiceImpl.whitelistService = whitelistService
        customerWapApiServiceImpl.guideFrontRelationWrapperService = guideFrontRelationWrapperService
        customerWapApiServiceImpl.relationApiService = relationApiService
        customerWapApiServiceImpl.customerRelationWriteApiService = customerRelationWriteApiService
        customerWapApiServiceImpl.shoppingGuideCheckApiService = shoppingGuideCheckApiService
        customerWapApiServiceImpl.shoppingGuideReadApiWrapperService = shoppingGuideReadApiWrapperService
    }

    @Unroll
    def "testBindCustomerRelation #caseName"() {
        given: "准备数据"
        def customerRelationSaveWapDTO = new CustomerRelationSaveWapDTO(bindSourceType: bindSourceType, kdtId: kdtId, sellerFrom: sellerFrom, buyerId: buyerId, shoppingGuideBindSourceType: shoppingGuideBindSourceType);

        and: "mock数据"
        shoppingGuideCheckApiService.isNormalShoppingGuide(_) >> isShoppingGuide;
        shoppingGuideReadApiWrapperService.getShoppingGuideByUserId(_, _) >> isPosShoppingGuide
        nsqPushService.pushNsq(_) >> null;
        whitelistService.inWhiteList(_, _) >> inWhiteList;
        guideFrontRelationWrapperService.bindCustomerRelationForWap(_) >> true;

        when: "调用接口"
        print("入参:--->" + customerRelationSaveWapDTO);
        def res = customerWapApiServiceImpl.bindCustomerRelation(customerRelationSaveWapDTO);
        print("返回结果--->" + res)

        then: "校验结果"
        code == res.code;
        data == res.data;
        message == res.message;


        where:
        caseName       | isShoppingGuide            | isPosShoppingGuide | inWhiteList | userId | shoppingGuideBindSourceType | bindSourceType | kdtId | buyerId | fansId | fansType | sellerFrom  | code      | data                | message
        "userId不存在"    | null                       | null               | false       | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | "sl"        | 300000000 | null                | "查询结果为空"
        "sl为空"         | null                       | null               | false       | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | null        | 200       | new StatusDTO(true) | "successful"
        "sl为undefined" | null                       | null               | false       | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | "undefined" | 200       | new StatusDTO(true) | "successful"
        "sl错误"         | null                       | null               | false       | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | "sajjv"     | 300000000 | null                | "查询结果为空"
        "切流新方法"        | ResultHelper.success(true) | null               | true        | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | "sajjv"     | 200       | new StatusDTO(true) | "successful"
        "未切流新方法"       | ResultHelper.success(true) | null               | false       | 1      | 1                           | 1              | 1     | 1       | 1      | 1        | "sajjv"     | 200       | new StatusDTO(false) | "successful"

    }

}

下面再来仔细介绍下spock框架

Spock测试框架

一、为什么要使用Spock

用JUnit写的单元测试

@Test

public void addPerson() {

    // 正常添加

    PersonVo personVo = PersonVo.builder()
        .idCardNo("1345")
        .name("Jack")
        .sex("male")
        .build();

    Assert.assertTrue(this.personService.addPerson(personVo));
    // 名字重复

    personVo = PersonVo.builder()
        .idCardNo("1346")
        .name("Jack")
        .sex("male")
        .build();

    Assert.assertFalse(this.personService.addPerson(personVo));
    // idCardNo重复
    personVo = PersonVo.builder()
        .idCardNo("1345")
        .name("Jack Chen")
        .sex("male")
        .build();
    Assert.assertFalse(this.personService.addPerson(personVo));

}

使用Spock编写同样的单元测试

@Unroll
def "addPerson:(idCardNo->#idCardNo, sex->#sex, name->#name), expect:#result"() {
    // 前置条件 同setup
    given:
    def personVo = PersonVo(
        idCardNo: idCardNo,
        name: name,
        sex: sex
    )

    // 预期
    expect:
    result == this.personService.addPerson(personVo)

    // 条件
    where:
    // 数据定义方法一
    // |用来分隔输入 ||用来分隔输出
    idCardNo | name   | sex      || result
    "5101"   | "Jack" | "male"   || true
    // idCardNo重复
    "5101"   | "John" | "male"   || false
    // name重复
    "5102"   | "Jack" | "male"   || false
    "123456" | "Lucy" | "female" || true

}

二、Spock基本概念

Specification:

测试类都必须继承Specification类

Fixture Methods

// 每个spec前置
def setupSpec() {

}

// 每个spec后置
def cleanupSpec() {

}

// 每个方法前置
def setup() {

}

// 每个方法后置
def cleanup() {

}

Feature methods

// 动态方法名
@Unroll
def "addPerson:(idCardNo->#idCardNo, sex->#sex, name->#name), expect:#result"() {

}

// 固定方法名
def addPerson(){

}

setup/given Blocks

在这个block中会放置与这个测试函数相关的初始化程序

given: // 也可以写作setup
def stack = new Stack()
def elem = "push me"

when and then Blocks

when:
stack.push(elem) 


then:
!stack.empty
stack.size() == 1
stack.peek() == elem

expect Blocks
when and then Blocks例子可以替换为:

given:
def stack = new Stack()
def elem = "push me"
stack.push(elem)


expect:
stack.empty == false
stack.size() == 1
stack.peek() == elem

where Blocks
做测试时最复杂的事情之一就是准备测试数据,尤其是要测试边界条件、测试异常分支等,这些都需要在测试之前规划好数据.

def "maximum of two numbers"() {
    expect:
    // exercise math method for a few different inputs
    Math.max(1, 3) == 3
    Math.max(7, 4) == 7
    Math.max(0, 0) == 0
}

// 可以替换为

def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c
    where:
    a | b || c
    3 | 5 || 5
    7 | 0 || 7
    0 | 0 || 0

}

三、Spock基本使用

class CalculateSpec extends Specification {

    // 每个spec前置
    def setupSpec() {
        calculateService = new CalculateService()
        println ">>>>>>   setupSpec"
    }

    // 每个方法前置
    def setup() {
        println ">>>>>>   setup"
    }

    // 每个方法后置
    def cleanup() {
        println ">>>>>>   cleanup"
    }

    // 每个spec后置
    def cleanupSpec() {
        println ">>>>>>   cleanupSpec"

    }



    def "test life cycle"() {
        given:
        def a = 1
        def b = 2

        expect:
        a < b
        println "test method finished!"
    }

}

基本标签语句组合形

  • given … expect … 
  • given … when … then …
  • when … then …
  • given … expect … where …
  • expect … where …
  • expect

with() 和 verifyAll()

def "test person use with(p)"() {

        given: "init a person"
        Date now = new Date()
        Person p = new Person(name: "yawn", age: 18, birthday:   now)

        expect: "测试p"

        with(p) {
            name == "yawn"
            age < 20
            birthday == now
        }
    }

spock异常处理

@Unroll

def "exception handle"() {
    when:
    ValidateUitls.validate(new SomeDTO(id: id, name: name))
    
    then:
    def exception = thrown(Exception)
    errMsg == exception.getMessage
    
    where:
    id           |  name      |  errMsg
    null         |  null      |  "some error message"

}

spring环境中使用spock

@SpringBootTest
@ContextConfiguration
class SpringBootSpec extends Specification {
    @Share
    CalculateService calculateService;

    def "spring boot test"() {

        expect: "asas"
        z == calculateService.minus(x, y)
        where:
        x << [9, 8, 7]
        y << [6, 5, 4]
        z << [3, 3, 3]

    }

    def "spring boot test2"() {

        expect: "asas"
        z == calculateService.minus(x, y)

        where:
        x | y | z
        9 | 8 | 1
        6 | 5 | 1
        3 | 3 | 0
    }

}

四、Spock数据驱动

Spock数据驱动测试( Data Driven Testing ),就是测试用例的书写格式更加面向数据,spock的数据驱动测试的书写格式,可即很清晰地汇集大量测试数据。


其中测试方法的参数 int a, int b, int c 称为数据变量(data variables),where 标签后的语句块称为数据表(data table)。

class MathSpec extends Specification {
  def "maximum of two numbers"(int a, int b, int c) {
    expect:
    Math.max(a, b) == c
    where:
    a | b | c
    1 | 3 | 3
    7 | 4 | 7
    0 | 0 | 0
  }
}

数据表(data table)

数据表(data table)就是一个看起来直观的存放测试数据的表格。数据表的第一行表头对应数据变量,从第二行开始就是数据行(data rows)。

如果测试方法只有一个数据变量a,则需要

where:

a | _

1 | _

7 | _

0 | _

数据表的简化写法

由于数据变量在被测试的语句Math.max(a, b) == c中有所体现,所以测试方法的参数可以省略掉;此外,被测试的方法Math.max(a, b) == c中,a,b为参数,c

def "maximum of two numbers"() {
    expect:
    Math.max(a, b) == c
    where:
    a | b || c
    1 | 3 || 3
    7 | 4 || 7
    0 | 0 || 0
}

where模块第一行代码是表格的列名,多个列使用|单竖线隔开,||双竖线区分输入和输出变量,即左边是输入值,右边是输出值。格式如下:

输入参数a | 输入参数b || 输出结果c

数据管道(Data Pipes)

其实,上面介绍的数据表只是数据管道的语法糖。数据管道(data pipes)就是给每一个数据变量提供一组值,写法如下:

...

where:

a << [170]

b << [340]

c << [370]

一个数据管道,由两部分组成,<<前面的是数据变量,<<后面的是数据来源( data provider )。其中 数据来源( data provider ) 可以为任何Groovy中可遍历的对象,包括  CollectionStringIterable等类型的对象和实现了 Iterable 的对象。

数据来源( data provider )也不要求是一定是类似的集合,它也可以是从外部的文本文件、数据库、电子表格等获取数据的代码,甚至是自动生成数据的代码片段。

多变量的数据管道(Multi-Variable Data Pipes)

如果一个数据来源(data provider)每次迭代可以提供多个变量,则可以将它做为多个数据变量的数据来源 ,形成一个多变量的数据管道(Multi-Variable Data Pipes)。如下:

@Shared sql = Sql.newInstance("jdbc:mysql://localhost:3306/test", "com.mysql.jdbc.Driver", "root", "root")

def "maximum of two numbers"() {
  expect:
  Math.max(a, b) == c
  where:
  [a, b, c] << sql.rows("select a, b, c from maxdata")
}

如果数据来源的某列返回值我们并不使用,则可以使用下划线“_”,这样就会被忽略,如下:

...

where:

[a, b, _, c] << sql.rows("select * from maxdata")

五、spock测试桩mock和stub的使用

 

如图,有如上的方法调用关系(模块依赖关系):A调用B和E方法,B调用C和D方法。在使用spock进行单元测试时,有如下情景,分别可使用stub和mock。

使用stub测试桩

如果我们需要测试A方法,但是E方法目前还没办法调用,或者还没开发完成。这种场景下,就可以使用stub测试桩。stub测试桩可以给E方法模拟一个或多个假的返回值,我们测试时只需要调用stub对象的E方法即可,调用后的返回值是我们在生成stub对象时指定的。如下:

def "Stub 测试桩"() {
        given: "构造测试桩"
        CalculateInterface calculateService = Stub(CalculateInterface)
        calculateService.plusPlus(_) >> 1

        when:
        int x = calculateService.plusPlus(12)
        int y = calculateService.plusPlus(3)

        then:
        x == 1
        y == 1

}

上面代码中,calculateService.plusPlus(_) >> 1 给一个并未实现的plusPlus()方法指定了返回值为1,测试代码就可以直接调用这个方法了。

其中这个语句的常用格式有:

subscriber.receive(_) >> "ok"

|          |       |     |

|          |       |     生成返回值

|          |       参数(多个参数可以使用:*_)

|          方法

对象

生成返回值

// 不同参数生成不同的返回值

subscriber.receive("message1") >> "ok"

subscriber.receive("message2") >> "fail"

// 生成多个返回值

subscriber.receive(_) >>> ["ok""error""error""ok"]

通过计算生成返回值

这种方式,生成返回值的格式时一个闭包

// 1.使用方法参数计算

subscriber.receive(_) >> { args -> args[0].size() > 3 "ok" "fail" }

// 2. 使用其他参数

subscriber.receive(_) >> { String message -> message.size() > 3 "ok" "fail" }

如果想调用方法抛出异常

subscriber.receive(_) >> { throw new InternalError("ouch") }

链式生成返回值

subscriber.receive(_) >>> ["ok""fail""ok"] >> { throw new InternalError() } >> "ok"

上面代码中,方法被调用的前三次分别返回 “ok”, “fail”, “ok”,第四次会抛出异常,第五次及以后调用,会返回“ok”。

以上是spock中stub测试桩的使用场景,总结为一句就是: stub测试桩给被调用者( 方法/模块)制造假的返回值,以便不影响调用者的测试。

使用mock测试桩

mock测试桩就是模拟一个测试的结果。如下图,A类调用类B和C类的某个方法:

 

如果要测试A的方法,但是我们没办法调用B来检测结果,就可以使用mock测试桩,生成一个B的mock对象。检验结果时,可以使用B的mock对象替代B。这个结果一般是B和C方法的调用或者状态的改变。

def subscriber = Mock(Subscriber)   // 1. 创建一个mock对象
    def "should send messages subscriber"() {
        when:
        publisher.send("hello")         // 2. publisher 发送一个“hello”
        then:
        1 * subscriber.receive("hello") // 3. subscriber 接收到一个“hello”
        1 * subscriber.messageCount == 1

}

mock和stub测试桩的对比

  • mock测试桩用于检测结果。
  • stub测试桩用于提供测试的条件。

六、Spock注解使用

@Share

在测试类中,Share标记的变量可以在不同的测试方法中使用。

@Ignore 忽略

  • 忽略测试方法

@IgnoreRest 忽略其他

  • 忽略其他测试方法

@Unroll 展开数据管道的测试用例

  • 展开:数据驱动测试中,展开所有的测试结果,分别显示每个测试用例的测试情况

@FailsWith(ArithmeticException.class) 标记失败

  • 记录已经知道的 bug
  • 标记让方法执行失败的测试用例

@Timeout(value = 10, unit = TimeUnit.MILLISECONDS) 超时时间设置

  • 超时就失败

@IgnoreIf 根据条件忽略

@IgnoreIf({ System.getProperty("os.name").contains("windows") })

def "I'll run everywhere but on Windows"() { ... }

@Requires 根据条件执行

@Requires({ os.windows })

def "I'll only run on Windows"() { ... }

@Retry 重试

@Retry(count 5)

@Unroll 展开

 

七、实战案例

1、数据驱动

 

2、stub测试桩

 

checkPayGroupBuilder.buildCheckPayResponseDTO(*_) >> new CheckPayResponseDTO(allOrderPaid: true)

|                    |                        |      |

|                    |                        |      生成返回值

|                    |                        参数(多个参数可以使用:*_)

|                    方法

对象

八、注意事项

1、新增ut

 

 

九、其他

spock官方文档

如何使用spock编写单元测试

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在进行Spock单元测试时,如果需要测试多表关联,可以使用Django提供的强大而直观的查询方式来处理关联关系。通过使用关联的模型字段的名称,并使用双下划线分隔,可以跨越关联关系,直到达到所需的字段。这样可以在后台自动处理JOIN操作。\[1\] 在进行多表关联查询时,可以使用内连接查询和外连接查询。内连接查询只会查询满足连接条件的数据,不满足连接条件的数据无法查询出来。隐式内连接查询可以使用"from 主表, 从表 where 从表的外键 = 主表的主键"的语法进行查询。显式内连接查询可以使用"from 主表 inner join 从表 on 从表的外键 = 主表的主键"的语法进行查询。\[3\] 如果需要进行全外连接查询,可以使用union联合查询来实现。全外连接查询会将左表和右表的数据都查询出来,并按照连接条件进行连接。\[2\] 在Spock单元测试中,可以根据具体的需求选择合适的连接方式来进行多表关联查询。可以使用Django提供的查询语法来编写测试代码,以验证多表关联的正确性。 #### 引用[.reference_title] - *1* [28.多表查询——跨关联关系的多表查询](https://blog.csdn.net/qq_44907926/article/details/120018985)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [MySQL的多表关联查询](https://blog.csdn.net/HunterArley/article/details/127685224)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^koosearch_v1,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值