Spock "then" 块下的魔法是什么?

What is the magic under Spock "then" block?

我是 Groovy/Spock 的新手,很想了解 "then" 块语法的秘密:

def "my specification"() {
    given:
        int a = 1
    when:
        ++a
    then:
        a == 1
        a > 0
        a != 2
}

框架如何分别评估此方法的最后 3 行?

then 本身就是一个普通的旧 Java 标签。

每一行都是一个新调用 >> Java 的 ; 可以省略。

每个调用都包含在 Groovy 的 assert 语句中,也可以在 Spock 外部执行:

int a = 1
assert a
assert 0 < a
assert !a // here assertion error

除了tim_yates and injecteer所说的,下面是Fernflower反编译(在IntelliJ IDEA中完成)的结果如下:

package de.scrum_master.Whosebug

import spock.lang.Specification

class DummyTest extends Specification {
  def "my specification"() {
    given:
    int a = 1
    when:
    ++a
    then:
    a == 2
    a > 1
    a != 3
  }
}
package de.scrum_master.Whosebug;

import groovy.lang.GroovyObject;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
import org.spockframework.runtime.ErrorCollector;
import org.spockframework.runtime.SpockRuntime;
import org.spockframework.runtime.ValueRecorder;
import org.spockframework.runtime.model.BlockKind;
import org.spockframework.runtime.model.BlockMetadata;
import org.spockframework.runtime.model.FeatureMetadata;
import org.spockframework.runtime.model.SpecMetadata;
import spock.lang.Specification;

@SpecMetadata(
  filename = "DummyTest.groovy",
  line = 5
)
public class DummyTest extends Specification implements GroovyObject {
  public DummyTest() {
    CallSite[] var1 = $getCallSiteArray();
    super();
  }

  @FeatureMetadata(
    line = 6,
    name = "my specification",
    ordinal = 0,
    blocks = {@BlockMetadata(
  kind = BlockKind.SETUP,
  texts = {}
), @BlockMetadata(
  kind = BlockKind.WHEN,
  texts = {}
), @BlockMetadata(
  kind = BlockKind.THEN,
  texts = {}
)},
    parameterNames = {}
  )
  public void $spock_feature_0_0() {
    CallSite[] var1 = $getCallSiteArray();
    ErrorCollector $spock_errorCollector = (ErrorCollector)ScriptBytecodeAdapter.castToType(var1[0].callConstructor(ErrorCollector.class, false), ErrorCollector.class);
    ValueRecorder $spock_valueRecorder = (ValueRecorder)ScriptBytecodeAdapter.castToType(var1[1].callConstructor(ValueRecorder.class), ValueRecorder.class);

    Object var10000;
    try {
      int a = 1;
      int a;
      if (BytecodeInterface8.isOrigInt() && !__$stMC && !BytecodeInterface8.disabledStandardMetaClass()) {
        a = a + 1;
      } else {
        a = DefaultTypeTransformation.intUnbox(var1[2].call(Integer.valueOf(a)));
      }

      try {
        SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "a == 2", Integer.valueOf(12), Integer.valueOf(5), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), a), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), 2))));
        var10000 = null;
      } catch (Throwable var44) {
        SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "a == 2", Integer.valueOf(12), Integer.valueOf(5), (Object)null, var44);
        var10000 = null;
      } finally {
        ;
      }

      try {
        SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "a > 1", Integer.valueOf(13), Integer.valueOf(5), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareGreaterThan($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), a), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), 1))));
        var10000 = null;
      } catch (Throwable var42) {
        SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "a > 1", Integer.valueOf(13), Integer.valueOf(5), (Object)null, var42);
        var10000 = null;
      } finally {
        ;
      }

      try {
        SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "a != 3", Integer.valueOf(14), Integer.valueOf(5), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareNotEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), a), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), 3))));
        var10000 = null;
      } catch (Throwable var40) {
        SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "a != 3", Integer.valueOf(14), Integer.valueOf(5), (Object)null, var40);
        var10000 = null;
      } finally {
        ;
      }

      var1[3].call(var1[4].call(this.getSpecificationContext()));
    } finally {
      $spock_errorCollector.validateCollectedErrors();
      var10000 = null;
    }

  }
}

Tim 的 link 为您提供了更多背景信息。如您所见,不仅仅是前置 assert,它更像是包裹在 try-catch 中的 SpockRuntime.verifyCondition(..)

从实施的不同方面来看,有几个很好的答案。我将提供另一个关于如何解析 Spock 语法的答案,并提供源代码链接。

首先,语法很简单"label". The official doc shows that labels are used similar to ancient C labels in goto. A statement can have label(s)。它是一种语法糖,Spock 使用它来提供直接的 DSL 语法。

groovy 源代码由 groovy 编译器处理(解析)并将源代码转换为 AST。这已经是一个有效的 groovy 程序,但它无法执行您期望的操作。 Spock 需要另一个转换(重写)来为语法添加单元测试功能。 Spock 为您编写“胶水代码”。

Spock 的规范访问者使用 buildBlocks() 检查标签并决定在 AST 中构建一个块供以后重写。

之后,重写器将 re-visit the AST (in your case visitThenBlock()) to generate code as other answers have shown. The rewriter checks then block to ensure that statements|expressions in the block are asserts or implicit conditions