Spock 不明确的断言行为

Spock unclear assertion behavior

据我所知,验证测试结果的一种方法是将表达式写入 then 部分,计算结果为布尔值。

但是最近我遇到了一种我不理解的行为。似乎当一个人试图验证某些东西时,即在一个块中,then 断言仅适用于显式 assert 关键字。

这是例子。我写了一个虚拟的 if 语句来有一个块,但它与 for 循环或任何控制流相同。

def "test fails as expected"() {
    when: "result has some value"
    def result = "someValue"

    then: "result has the expected value"
    result == "otherValue"
}

def "test passes, but shouldn't"() {
    when: "result has some value"
    def result = "someValue"

    then: "result has the expected value"
    if (true) {
        result == "otherValue"
    }
}

def "test fails as expected when using assert"() {
    when: "result has some value"
    def result = "someValue"

    then: "result has the expected value"
    if (true) {
        assert result == "otherValue"
    }
}

我发现这种行为有点误导。有人可以解释为什么它会这样吗?这是错误还是用法不正确?

正在关注 Spock documentation

The when and then blocks always occur together. They describe a stimulus and the expected response. Whereas when blocks may contain arbitrary code, then blocks are restricted to conditions, exception conditions, interactions, and variable definitions. A feature method may contain multiple pairs of when-then blocks.

这解释了为什么 Spocks AST 转换器看不到以下 then 块:

then:
if (true) {
    result == "otherValue"
}

作为正确的调用,它不会将其转换为 SpockRuntime.verifyCondition() 调用。

如果您编译 class(启用静态编译以提高可读性)并检查字节码,您将看到类似于此的内容:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import groovy.lang.GroovyObject;
import org.codehaus.groovy.runtime.DefaultGroovyMethods;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
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 = "OtherSpec.groovy",
    line = 4
)
public class OtherSpec extends Specification implements GroovyObject {
    public OtherSpec() {
    }

    public Object test(String result) {
        return true ? ScriptBytecodeAdapter.compareEqual(result, "otherValue") : null;
    }

    @FeatureMetadata(
        line = 7,
        name = "test fails as expected",
        ordinal = 0,
        blocks = {@BlockMetadata(
    kind = BlockKind.WHEN,
    texts = {"result has some value"}
), @BlockMetadata(
    kind = BlockKind.THEN,
    texts = {"result has the expected value"}
)},
        parameterNames = {}
    )
    public void $spock_feature_0_0() {
        ErrorCollector $spock_errorCollector = new ErrorCollector(false);
        ValueRecorder $spock_valueRecorder = new ValueRecorder();

        Object var10000;
        try {
            String result = "someValue";

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

            ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
        } finally {
            $spock_errorCollector.validateCollectedErrors();
            var10000 = null;
        }

    }

    @FeatureMetadata(
        line = 15,
        name = "test passes, but shouldn't",
        ordinal = 1,
        blocks = {@BlockMetadata(
    kind = BlockKind.WHEN,
    texts = {"result has some value"}
), @BlockMetadata(
    kind = BlockKind.THEN,
    texts = {"result has the expected value"}
)},
        parameterNames = {}
    )
    public void $spock_feature_0_1() {
        String result = "someValue";
        if (true) {
            ScriptBytecodeAdapter.compareEqual(result, "otherValue");
        }

        ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
    }

    @FeatureMetadata(
        line = 25,
        name = "test fails as expected when using assert",
        ordinal = 2,
        blocks = {@BlockMetadata(
    kind = BlockKind.WHEN,
    texts = {"result has some value"}
), @BlockMetadata(
    kind = BlockKind.THEN,
    texts = {"result has the expected value"}
)},
        parameterNames = {}
    )
    public void $spock_feature_0_2() {
        ErrorCollector $spock_errorCollector = new ErrorCollector(false);
        ValueRecorder $spock_valueRecorder = new ValueRecorder();

        Object var10000;
        try {
            String result = "someValue";
            DefaultGroovyMethods.println(this, this.test("otherValue"));
            var10000 = null;
            if (true) {
                try {
                    SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
                    var10000 = null;
                } catch (Throwable var13) {
                    SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, var13);
                    var10000 = null;
                } finally {
                    ;
                }
            }

            ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
        } finally {
            $spock_errorCollector.validateCollectedErrors();
            var10000 = null;
        }

    }
}

现在,如果我们分析这段代码,我们会发现下面的 Spock 测试用例:

def "test fails as expected"() {
    when: "result has some value"
    def result = "someValue"

    then: "result has the expected value"
    result == "otherValue"
}

编译成这样的东西:

public void $spock_feature_0_0() {
    ErrorCollector $spock_errorCollector = new ErrorCollector(false);
    ValueRecorder $spock_valueRecorder = new ValueRecorder();

    Object var10000;
    try {
        String result = "someValue";

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

        ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
    } finally {
        $spock_errorCollector.validateCollectedErrors();
        var10000 = null;
    }

}

以及将断言放入 if 语句的测试用例:

def "test passes, but shouldn't"() {
    when: "result has some value"
    def result = "someValue"

    then: "result has the expected value"
    if (true) {
        result == "otherValue"
    }
}

编译成这样的东西:

public void $spock_feature_0_1() {
    String result = "someValue";
    if (true) {
        ScriptBytecodeAdapter.compareEqual(result, "otherValue");
    }

    ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
}

If you are interested in investigating the source code of this AST transformation you can start by analyzing:

对于最后一个用例——将 assert 添加到 if 语句块是对 Spock 的明确指示,它必须转换为验证条件调用。这就是为什么你会看到反编译为这样的字节码:

public void $spock_feature_0_2() {
    ErrorCollector $spock_errorCollector = new ErrorCollector(false);
    ValueRecorder $spock_valueRecorder = new ValueRecorder();

    Object var10000;
    try {
        String result = "someValue";
        DefaultGroovyMethods.println(this, this.test("otherValue"));
        var10000 = null;
        if (true) {
            try {
                SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));
                var10000 = null;
            } catch (Throwable var13) {
                SpockRuntime.conditionFailedWithException($spock_errorCollector, $spock_valueRecorder, "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, var13);
                var10000 = null;
            } finally {
                ;
            }
        }

        ScriptBytecodeAdapter.invokeMethod0(OtherSpec.class, ((OtherSpec)this).getSpecificationContext().getMockController(), (String)"leaveScope");
    } finally {
        $spock_errorCollector.validateCollectedErrors();
        var10000 = null;
    }
}

请注意 if (true) { /*...*/ } 仍然存在,因为 AST 转换器仍然忽略对其进行转换,但是条件:

assert result == "otherValue"

被 AST 转换器访问并接受,并替换为:

SpockRuntime.verifyCondition($spock_errorCollector, $spock_valueRecorder.reset(), "result == \"otherValue\"", Integer.valueOf(32), Integer.valueOf(20), (Object)null, $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(2)), ScriptBytecodeAdapter.compareEqual($spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(0)), result), $spock_valueRecorder.record($spock_valueRecorder.startRecordingValue(Integer.valueOf(1)), "otherValue"))));