Spock in Java 慢慢爱上写单元测试 2

发表于:2022-9-28 09:18

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:Richard_Yi    来源:稀土掘金

  Spock快速使用
  现在让我们以最快速的方式,来使用一次Spock。
  3.0 创建一个空白项目
  创建一个空白项目:spock-example,选择maven工程。
  3.1 依赖
    <dependencies>
      <!-- Mandatory dependencies for using Spock -->
      <!-- 使用Spock必须的依赖 -->
      <dependency>
        <groupId>org.spockframework</groupId>
        <artifactId>spock-core</artifactId>
        <version>1.3-groovy-2.5</version>
        <scope>test</scope>
      </dependency>
      <!-- Optional dependencies for using Spock -->
      <!-- 选择性使用的Spock相关依赖 -->
      <dependency> <!-- use a specific Groovy version rather than the one specified by spock-core -->
      <!-- 不使用Spock-core中定义的Groovy版本,而是自己定义 -->
        <groupId>org.codehaus.groovy</groupId>
        <artifactId>groovy-all</artifactId>
        <version>2.5.7</version>
        <type>pom</type>
      </dependency>
      <dependency> <!-- enables mocking of classes (in addition to interfaces) -->
        <!-- mock 接口和类时要用 -->
        <groupId>net.bytebuddy</groupId>
        <artifactId>byte-buddy</artifactId>
        <version>1.9.3</version>
        <scope>test</scope>
      </dependency>
      <dependency> <!-- enables mocking of classes without default constructor (together with CGLIB) -->
        <!-- mock 类要用 -->
        <groupId>org.objenesis</groupId>
        <artifactId>objenesis</artifactId>
        <version>2.6</version>
        <scope>test</scope>
      </dependency>
      <dependency> <!-- only required if Hamcrest matchers are used -->
        <!-- Hamcrest 是一个用于编写匹配对象的框架,如果用到了Hamcrest matchers,需要加这个依赖 -->
        <groupId>org.hamcrest</groupId>
        <artifactId>hamcrest-core</artifactId>
        <version>1.3</version>
        <scope>test</scope>
      </dependency>
      <!-- Dependencies used by examples in this project (not required for using Spock) -->
      <!-- 使用h2base做测试数据库-->
      <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <version>1.4.197</version>
        <scope>test</scope>
      </dependency>
    </dependencies>
  3.2  插件
    <plugins>
        <!-- Mandatory plugins for using Spock -->
        <!--使用Spock的强制性插件 -->
        <plugin>
          <!-- The gmavenplus plugin is used to compile Groovy code. To learn more about this plugin,visit https://github.com/groovy/GMavenPlus/wiki -->
          <!-- 这个 gmavenplus 插件是用于编译Groovy代码的 . 想获取更多此插件相关信息,visit https://github.com/groovy/GMavenPlus/wiki -->
          <groupId>org.codehaus.gmavenplus</groupId>
          <artifactId>gmavenplus-plugin</artifactId>
          <version>1.6</version>
          <executions>
            <execution>
              <goals>
                <goal>compile</goal>
                <goal>compileTests</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
        <!-- Optional plugins for using Spock -->
        <!-- 选择性使用的Spock相关插件-->
        <!-- Only required if names of spec classes don't match default Surefire patterns (`*Test` etc.) -->
        <!--只有当测试类不匹配默认的 Surefire patterns (`*Test` 等等.)-->
        <plugin>
          <artifactId>maven-surefire-plugin</artifactId>
          <version>2.20.1</version>
          <configuration>
            <useFile>false</useFile>
            <includes>
              <include>**/*Test.java</include>
              <include>**/*Spec.java</include>
            </includes>
          </configuration>
        </plugin>
    ...
      </plugins>
  3.3 设计测试源码目录
  由于spock是基于groovy语言的,所以需要创建groovy的测试源码目录:首先在test目录下创建名为groovy的目录,之后将它设为测试源码目录。
  3.4 编写待测试类
  /**
   * @author Richard_yyf
   * @version 1.0 2019/10/1
   */
  public class Calculator {
      public int size(String str){
          return str.length();
      }
      
      public int sum(int a, int b) {
          return a + b;
      }
  }
  3.5 创建测试类
  Ctrl + Shift + T
  import spock.lang.Specification
  import spock.lang.Subject
  import spock.lang.Title
  import spock.lang.Unroll
  /**
   *
   * @author Richard_yyf
   * @version 1.0 2019/10/1
   */
  @Title("测试计算器类")
  @Subject(Calculator)
  class CalculatorSpec extends Specification {
      def calculator = new Calculator()
      void setup() {
      }
      void cleanup() {
      }
      def "should return the real size of the input string"() {
          expect:
          str.size() == length
          where:
          str     | length
          "Spock"  | 5
          "Kirk"   | 4
          "Scotty" | 6
      }
      // 测试不通过
      def "should return a+b value"() {
          expect:
          calculator.sum(1,1) == 1
      }
      // 不建议用中文哦
      @Unroll
      def "返回值为输入值之和"() {
          expect:
          c == calculator.sum(a, b)
          where:
          a | b | c
          1 | 2 | 3
          2 | 3 | 5
          10 | 2 | 12
      }
  }
  3.6 运行测试
  3.7 模拟依赖
  这里模拟一个缓存服务作为例子。
  /**
   * @author Richard_yyf
   * @version 1.0 2019/10/2
   */
  public interface CacheService {
      String getUserName();
  }
  public class Calculator {
      private CacheService cacheService;
      public Calculator(CacheService cacheService) {
          this.cacheService = cacheService;
      }
      public boolean isLoggedInUser(String userName) {
          return Objects.equals(userName, cacheService.getUserName());
      }
      ...
  }
  测试类
  class CalculatorSpec extends Specification {
      
      // mock对象
  //    CacheService cacheService = Mock()
      def cacheService = Mock(CacheService)
      def calculator
      void setup() {
         calculator = new Calculator(cacheService)
      }
      def  "is username equal to logged in username"() {
          // stub 打桩
          cacheService.getUserName(*_) >> "Richard"
          when:
          def result = calculator.isLoggedInUser("Richard")
          then:
          result
      }
      ...
  }
  运行测试:
  Spock 深入
  在Spock中,待测系统(system under test; SUT) 的行为是由规格(specification) 所定义的。在使用Spock框架编写测试时,测试类需要继承自Specification类。命名遵循Java规范。
  Spock 基础结构
  每个测试方法可以直接用文本作为方法名,方法内部由given-when-then的三段式块(block)组成。除此以外,还有and、where、expect等几种不同的块。
  @Title("测试的标题")
  @Narrative("""关于测试的大段文本描述""")
  @Subject(Adder)  //标明被测试的类是Adder
  @Stepwise  //当测试方法间存在依赖关系时,标明测试方法将严格按照其在源代码中声明的顺序执行
  class TestCaseClass extends Specification {  
    @Shared //在测试方法之间共享的数据
    SomeClass sharedObj
   
    def setupSpec() {
      //TODO: 设置每个测试类的环境
    }
   
    def setup() {
      //TODO: 设置每个测试方法的环境,每个测试方法执行一次
    }
   
    @Ignore("忽略这个测试方法")
    @Issue(["问题#23","问题#34"])
    def "测试方法1" () {
      given: "给定一个前置条件"
      //TODO: code here
      and: "其他前置条件"
   
   
      expect: "随处可用的断言"
      //TODO: code here
      when: "当发生一个特定的事件"
      //TODO: code here
      and: "其他的触发条件"
   
      then: "产生的后置结果"
      //TODO: code here
      and: "同时产生的其他结果"
   
      where: "不是必需的测试数据"
      input1 | input2 || output
       ...   |   ...  ||   ...   
    }
   
    @IgnoreRest //只测试这个方法,而忽略所有其他方法
    @Timeout(value = 50, unit = TimeUnit.MILLISECONDS)  // 设置测试方法的超时时间,默认单位为秒
    def "测试方法2"() {
      //TODO: code here
    }
   
    def cleanup() {
      //TODO: 清理每个测试方法的环境,每个测试方法执行一次
    }
   
    def cleanupSepc() {
      //TODO: 清理每个测试类的环境
    }
  Feature methods
  是Spock规格(Specification)的核心,其描述了SUT应具备的各项行为。每个Specification都会包含一组相关的Feature methods:
      def "should return a+b value"() {
          expect:
          calculator.sum(1,1) == 1
      }
  blocks
  每个feature method又被划分为不同的block,不同的block处于测试执行的不同阶段,在测试运行时,各个block按照不同的顺序和规则被执行,如下图:
  Setup Blocks
  setup也可以写成given,在这个block中会放置与这个测试函数相关的初始化程序,如:
      def  "is username equal to logged in username"() {
          setup:
          def str = "Richard"
          // stub 打桩
          cacheService.getUserName(*_) >> str
          when:
          def result = calculator.isLoggedInUser("Richard")
          then:
          result
      }
  When and Then Blocks
  when与then需要搭配使用,在when中执行待测试的函数,在then中判断是否符合预期。
  Expect Blocks
  expect可以看做精简版的when+then,如
  when:
  def x = Math.max(1, 2)  
  then:
  x == 2
  码简化成
  expect:
  Math.max(1, 2) == 2   
  断言
  条件类似junit中的assert,就像上面的例子,在then或expect中会默认assert所有返回值是boolean型的顶级语句。如果要在其它地方增加断言,需要显式增加assert关键字
  异常断言
  如果要验证有没有抛出异常,可以用thrown()
    def "peek"() {
      when: stack.peek()
      then: thrown(EmptyStackException)
    }
  如果要验证没有抛出某种异常,可以用notThrown()
  Mock
  Mock 是描述规范下的对象与其协作者之间(强制)交互的行为。
  1 * subscriber.receive("hello")
  |   |          |       |
  |   |          |       argument constraint
  |   |          method constraint
  |   target constraint
  cardinality
  创建 Mock 对象
  def subscriber = Mock(Subscriber)
  def subscriber2 = Mock(Subscriber)
      
  Subscriber subscriber = Mock()
  Subscriber subscriber2 = Mock()    
  注入 Mock 对象
  class PublisherSpec extends Specification {
    Publisher publisher = new Publisher()
    Subscriber subscriber = Mock()
    Subscriber subscriber2 = Mock()
    def setup() {
      publisher.subscribers << subscriber // << is a Groovy shorthand for List.add()
      publisher.subscribers << subscriber2
    }
  调用频率约束(cardinality)
  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
                                       // (rarely needed; see 'Strict Mocking')
  目标约束(target constraint)
  1 * subscriber.receive("hello") // a call to 'subscriber'
  1 * _.receive("hello")          // a call to any mock object
  方法约束(method constraint)
  1 * subscriber.receive("hello") // a method named 'receive'
  1 * subscriber./r.*e/("hello")  // a method whose name matches the given regular expression (here: method name starts with 'r' and ends in 'e')
  参数约束(argument constraint)
  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') })
  // an argument that satisfies the given predicate, meaning that
  // code argument constraints need to return true of false
  // depending on whether they match or not
  // (here: message length is greater than 3 and contains the character a)
  Stub 打桩
  Stubbing 是让协作者以某种方式响应方法调用的行为。在对方法进行存根化时,不关心该方法的调用次数,只是希望它在被调用时返回一些值,或者执行一些副作用。
  subscriber.receive(_) >> "ok"
  |          |       |     |
  |          |       |     response generator
  |          |       argument constraint
  |          method constraint
  target constraint
  如:subscriber.receive(_) >> "ok" 意味,不管什么实例,什么参数,调用 receive 方法皆返回字符串 ok。
  返回固定值
  使用 >> 操作符,返回固定值。
  subscriber.receive(_) >> "ok"
  返回值序列
  返回一个序列,迭代且依次返回指定值。如下所示,第一次调用返回 ok,第二次调用返回 error,以此类推。
  subscriber.receive(_) >>> ["ok", "error", "error", "ok"]
  动态计算返回值
  subscriber.receive(_) >> { args -> args[0].size() > 3 ? "ok" : "fail" }
  subscriber.receive(_) >> { String message -> message.size() > 3 ? "ok" : "fail" }
  产生副作用
  subscriber.receive(_) >> { throw new InternalError("ouch") }
  链式响应
  subscriber.receive(_) >>> ["ok", "fail", "ok"] >> { throw new InternalError() } >> "ok"
  结语
  本文介绍了单元测试的基础知识,和Spock的一些用法。使用Spock,可以享受到groovy脚本语言的方便、一站式的测试套件,写出来的测试代码也更加优雅、可读。
  但是这只是第一步,学会了如何使用一个测试框架,只是初步学会了“术”而已,要如何利用好Spock,需要很多软性方面的改变,比如如何写好一个测试用例,如何渐进式地去重构代码和写出更易测试的代码,如何让团队实行TDD等等。
  本文内容不用于商业目的,如涉及知识产权问题,请权利人联系51Testing小编(021-64471599-8017),我们将立即处理
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号