基于Spock框架进行单元测试

以Spock测试框架为例,实现外部环境零依赖。(不依赖Spring/db等)

Spock

基于groovy的单元测试框架。

Specification

在Spock中,待测系统(SUT)的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类需要继承自Specification类。

Fixture Methods

def setup() {} :每个测试运行前的启动方法
def cleanup() {} : 每个测试运行后的清理方法
def setupSpec() {} : 第一个测试运行前的启动方法
def cleanupSpec() {} : 最后一个测试运行后的清理方法

Feature methods

这是Spock规格(Specification)的核心,其描述了SUT应具备的各项行为。每个Specification都会包含一组相关的Feature methods,如要测试1+1是否等于2,可以编写一个函数:

class DemoTest extends Specification {
     def setup() {
     
     }
     
    def "sum should return param1+param2" (){
        expect:
        sum.sum(1,1)==2
    }
}

blocks

每个feature method又被划分为不同的block,不同的block处于测试执行的不同阶段,在测试运行时,各个block按照不同的顺序和规则被执行,如下图:

where: 以表格的形式提供测试数据集合 when: 触发行为,比如调用指定方法或函数
then: 做出断言表达式
expect: 期望的行为,when-then的精简版
setup/given: 数据准备模块,mock单测中指定mock数据
cleanup: 释放资源模块

def "HashMap accepts null key"() {
  given:
  def map = new HashMap()

  when:
  map.put(null, "elem")

  then:
  notThrown(NullPointerException)
}

Where Blocks

//多值验证传统写法
class MathSpec extends Specification {
    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
    }
}


//通过where block可以让上面的测试实现起来变得非常优雅,此例子实际会跑三次测试
classDataDrivenextendsSpecification{
    def"maximum of two numbers"(){
        expect:
        Math.max(a,b)==c
 
        where:
        a|b||c
        3|5||5
        7|0||7
        0|0||0
    }
}

//可以为标记@Unroll的方法声明动态的spec名,运行时,名称会被替换为实际的参数值。
class DataDriven extends Specification {
    @Unroll
    def "maximum of #a and #b should be #c"() {
        expect:
        Math.max(a, b) == c
 
        where:
        a | b || c
        3 | 5 || 5
        7 | 0 || 7
        0 | 0 || 0
    }
}

spock demo

待测试代码:

public class Publisher {
    List<Subscriber> subscribers = new ArrayList<>();
    void send(String message) {
        for (Subscriber subscriber : subscribers) {
            String ret = subscriber.receive(message);
            if (!"ok".equals(ret)) {
                throw new RuntimeException("发送失败");
            }
        }
    }
}

public interface Subscriber {
    String receive(String message);
}

public class SubScriber1 implements Subscriber {
    @Override
    public String receive(String message) {
        System.out.println("subScriber1 receive message:" + message);
        return "ok";
    }
}

public class SubScriber2 implements Subscriber {
    @Override
    public String receive(String message) {
        System.out.println("subScriber2 receive message:" + message);
        return "ok";
    }
}

 

Stubbing

stub存在的意图是为了让测试对象可以正常的执行,其实现一般会硬编码一些输入和输出。

class PublisherTest extends Specification {

    Publisher publisher = new Publisher()
    Subscriber subscriber1 = Mock(Subscriber)
    Subscriber subscriber2 = Mock(Subscriber)

    def setup() {
        publisher.subscribers.add(subscriber1)
        publisher.subscribers.add(subscriber2)
    }

  def "should send messages to all subscribers with return val"() {
        when:
        publisher.send("hello")

        then:
        subscriber1.receive(_) >> "ok"
        subscriber2.receive("hello") >> "ok"
        notThrown(RuntimeException)
    }
}

多种返回值形式

//多次调用不同的值,返回数组
subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
//抛出异常
subscriber.receive(_) >> { throw new InternalError("ouch") }
//Method responses can be chained:
subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"`

Mocking

mock除了保证stub的功能之外,还可深入的模拟对象之间的交互方式,如:调用次数约束、方法名称约束、参数约束等。

class PublisherTest extends Specification {

    Publisher publisher = new Publisher()
    Subscriber subscriber1 = Mock(Subscriber)
    Subscriber subscriber2 = Mock(Subscriber)

    def setup() {
        publisher.subscribers.add(subscriber1)
        publisher.subscribers.add(subscriber2)
    }

    def "should send messages to all subscribers"() {
        when:
        publisher.send("hello")

        then:
        1 * subscriber1.receive("hello")
        0 * subscriber2.receive("hello")
        thrown(RuntimeException)
    }
}

表达式中的次数、对象、函数和参数部分都可以灵活定义

1 * subscriber.receive("hello")      // exactly one call
0 * subscriber.receive("hello")      // zero calls
(1..3) * subscriber.receive("hello") // between one and three calls (inclusive)
(1.._) * subscriber.receive("hello") // at least one call
(_..3) * subscriber.receive("hello") // at most three calls
_ * subscriber.receive("hello")      // any number of calls, including zero
1 * subscriber.receive("hello") // a call to 'subscriber'
1 * _.receive("hello")          // a call to any mock object
1 * subscriber.receive("hello") // a method named 'receive'
1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression
1 * subscriber.status // same as: 1 * subscriber.getStatus()
1 * subscriber.setStatus("ok") // NOT: 1 * subscriber.status = "ok"
1 * subscriber.receive("hello")        // an argument that is equal to the String "hello"
1 * subscriber.receive(!"hello")       // an argument that is unequal to the String "hello"
1 * subscriber.receive()               // the empty argument list (would never match in our example)
1 * subscriber.receive(_)              // any single argument (including null)
1 * subscriber.receive(*_)             // any argument list (including the empty argument list)
1 * subscriber.receive(!null)          // any non-null argument
1 * subscriber.receive(_ as String)    // any non-null argument that is-a String
1 * subscriber.receive(endsWith("lo")) // any non-null argument that is-a String
1 * subscriber.receive({ it.size() > 3 && it.contains('a') })
//Argument constraints work as expected for methods with multiple arguments
1 * process.invoke("ls", "-a", _, !null, { ["abcdefghiklmnopqrstuwx1"].contains(it) }) 

Mocking & Stubbing

def "should send messages to all subscribers with return val 1"() {
        when:
        publisher.send("hello")

        then:
        1 * subscriber1.receive("hello") >> "ok"
        1 * subscriber2.receive("hello") >> "ok"
        notThrown(RuntimeException)
}

Mocking & Stubbing & 验证入参

class PublisherTest extends Specification {

    Publisher publisher = new Publisher()
    Subscriber subscriber1 = Mock(Subscriber)
    Subscriber subscriber2 = Mock(Subscriber)

    def setup() {
        publisher.subscribers.add(subscriber1)
        publisher.subscribers.add(subscriber2)
    }
    
     def "should send messages to all subscribers with return val and unknow args"() {
        given:
        String message1 = null
        String message2 = null
        1 * subscriber1.receive(_) >> {
            arguments ->
                Object obj = arguments[0]
                message1 = (String) obj
                return "ok"
        }
        1 * subscriber2.receive(_) >> {
            arguments ->
                Object obj = arguments[0]
                message2 = (String) obj
                return "ok"
        }
        when:
        publisher.send("hello word")
        then:
        message1 == "hello word"
        message2 == "hello word"
        notThrown(RuntimeException)
    }
}

 

posted @ 2023-07-17 10:27  仟仟绾绾  阅读(348)  评论(0)    收藏  举报