【问题标题】:Spock unclear assertion behaviorSpock 不明确的断言行为
【发布时间】:2018-12-12 13:32:57
【问题描述】:

据我所知,验证测试结果的一种方法是将表达式写入 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"
    }
}

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

【问题讨论】:

  • I wrote a dummy if statement to have a block, but it's the same with a for loop or any control flow. 是什么意思?我无法理解这一点。并且您的第一个测试用例代码有效并且测试失败。顺便说一句then: "result has the expected value" 相当于then: true - 你为什么把字符串放在那里? Groovy Truth 将它们评估为 true。
  • 把Stirng放在那里就像在java单元测试中编写cmets一样。它与功能无关。我写 if 的原因如下。如您所见,前两个测试之间的区别仅在于,在第二个测试的情况下,“断言”位于 if 语句中,并带有 {}

标签: unit-testing groovy spock


【解决方案1】:

关注Spock documentation

whenthen 块总是一起出现。他们描述了刺激和预期的反应。 when 块可能包含任意代码,then 块仅限于条件异常条件交互和变量定义。一个特征方法可能包含多对when-then块。

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

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

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

如果您编译您的类(启用静态编译以获得更好的可读性)并检查字节码,您将看到类似于以下内容:

//
// 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");
}

如果您有兴趣研究此 AST 转换的源代码,您可以从分析开始:

对于最后一个用例 - 将 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"))));

【讨论】:

  • 文档解释了它,但也感谢链接和代码。看看它是如何工作的很有趣。很难争辩,因为这似乎是一种故意行为,但我仍然觉得它有点误导,因为它不会引发任何异常,除了验证之外,将布尔表达式写入 then 块对我来说真的没有意义。
猜你喜欢
  • 2015-04-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-19
相关资源
最近更新 更多