【问题标题】:Spock Unroll seems to print something odd with boolean parameterSpock Unroll 似乎用布尔参数打印了一些奇怪的东西
【发布时间】:2020-04-04 18:05:33
【问题描述】:

我只是把这个测试方法放在一起:

@Unroll
def 'super start edit should be called if cell is not empty'( boolean empty ){
    given:
    DueDateEditor editor = GroovySpy( DueDateEditor ){
        isEmpty() >> empty
    }

    when:
    editor.startEdit()

    then:
    if( empty){
        0 * editor.callSuperStartEdit()
    }
    else {
        1 * editor.callSuperStartEdit()
    }

    where:
    empty | _
    true  | _
    false | _
}

...就两个测试通过而言,它工作正常...但是当你让它失败时,它很奇怪:如果参数emptyfalse 的输出是

super start edit should be called if cell is not empty[1]

...如果参数emptytrue,则为0。这是一个错误吗?

【问题讨论】:

    标签: testing groovy parameters spock unroll


    【解决方案1】:

    我正在写一个额外的答案,因为

    • Tim 的解决方案中有一个关于标题的小错误(但他的回答在技术上仍然是绝对正确的!),
    • 这里不需要GroovySpy,简单的Spy 就足够了,
    • 我想向您展示另一种无需存根的测试方法isEmpty()
    • 我想向您展示如何在三元表达式而不是 if-else 中仅使用一次交互与调用次数(即使错误报告很丑陋),
    • 我想对您的一般测试方式发表评论(请参阅本文末尾)。
    package de.scrum_master.stackoverflow.q61032514;
    
    import java.time.LocalDate;
    
    public class DueDateEditor {
      String text;
    
      public boolean isEmpty() {
        return text == null || text.trim() == "";
      }
    
      public void startEdit() {
        if (!isEmpty())
          callSuperStartEdit();
      }
    
      public void callSuperStartEdit() {}
    }
    
    package de.scrum_master.stackoverflow.q61032514
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    class DueDateEditorTest extends Specification {
      @Unroll
      def 'super start edit #shouldMsg be called if the cell is #cellStateMsg'() {
        given:
        DueDateEditor editor = Spy() {
          isEmpty() >> empty
        }
    
        when:
        editor.startEdit()
    
        then:
        (empty ? 0 : 1) * editor.callSuperStartEdit()
    
        where:
        empty << [true, false]
        shouldMsg = empty ? 'should not' : 'should'
        cellStateMsg = empty ? 'empty' : 'not empty'
      }
    
      @Unroll
      def "super start edit #shouldMsg be called if cell text is '#text'"() {
        given:
        DueDateEditor editor = Spy()
        editor.text = text
    
        when:
        editor.startEdit()
    
        then:
        (editor.isEmpty() ? 0 : 1) * editor.callSuperStartEdit()
        // Or, if 'isEmpty()' has a side effect:
        // (text ? 1 : 0) * editor.callSuperStartEdit()
    
        where:
        text << ["foo", "", null, "line 1\nline 2"]
        shouldMsg = text ? 'should' : 'should not'
        cellStateMsg = text ? 'not empty' : 'empty'
      }
    }
    

    一般说明:

    • 我不会通过交互来测试单个类的内部连接。测试会很脆弱,如果您在内部重构类而不更改 API,如果交互不再符合预期,测试可能会中断。我认为这是过度规范,我只会将交互测试用于不同类之间的关键交互,或者可能是一个类的不同实例——“关键”意味着像观察者这样的设计模式的功能。
    • 如果整个测试只准确地知道这两种情况,那么使用 if-else 来区分两种情况,这两种情况只会使测试的可读性降低且更复杂,请查看您自己的代码以及我的和 Tim 的代码。在这种情况下,我宁愿编写两个具有简单标题和简单功能的特征方法,但没有 if-else 或三元表达式,没有标题的辅助变量等。

    P.S.:抱歉,我必须在测试中创建一个示例类 DueDateEditor 才能使我的测试按预期编译和运行。像往常一样,不幸的是,迈克没有提供MCVE,而只是提供了其中的一部分。


    更新:我们在 cmets 中谈到了 GroovySpy,正如我所说,如果您的类是 Java 类并且您想要存根有一个 final 方法,那么它将不起作用,请参阅Spock manual。这是给你的证据:

    package de.scrum_master.stackoverflow.q61032514;
    
    public class TreeTableCell<A, B> {
      String text;
    
      public final boolean isEmpty() {
        return text == null || text.trim() == "";
      }
    }
    
    package de.scrum_master.stackoverflow.q61032514;
    
    import java.time.LocalDate;
    
    public class DueDateEditor extends TreeTableCell<String, LocalDate> {
      public void startEdit() {
        if (!isEmpty())
          callSuperStartEdit();
      }
    
      public void callSuperStartEdit() {}
    }
    
    package de.scrum_master.stackoverflow.q61032514
    
    import spock.lang.Specification
    import spock.lang.Unroll
    
    class DueDateEditorTest extends Specification {
      @Unroll
      def 'super start edit #shouldMsg be called if the cell is #cellStateMsg'() {
        given:
        DueDateEditor editor = GroovySpy() {
          isEmpty() >> empty
        }
    
        when:
        editor.startEdit()
    
        then:
        (empty ? 0 : 1) * editor.callSuperStartEdit()
    
        where:
        empty << [true, false]
        shouldMsg = empty ? 'should not' : 'should'
        cellStateMsg = empty ? 'empty' : 'not empty'
      }
    }
    

    如果您的应用程序类仅是 Groovy 类,则该测试将起作用。但如果它们是我的示例中的 Java 类,则测试将失败,如下所示:

    Too few invocations for:
    
    (empty ? 0 : 1) * editor.callSuperStartEdit()   (0 invocations)
    
    Unmatched invocations (ordered by similarity):
    
    1 * editor.startEdit()
    methodName == "callSuperStartEdit"
    |          |
    startEdit  false
               10 differences (44% similarity)
               (s---------)tartEdit
               (callSuperS)tartEdit
    

    所以在这种情况下,您不能只使用 Groovy 魔法来检查交互。但正如我所说,无论如何你都不应该那样做。而是确保startEdit()callSuperStartEdit() 都做正确的事情。检查他们的结果,或者,如果他们是 void,检查他们对被测对象或其合作者状态的副作用。


    更新 2:关于您最初关于索引方法命名的问题,实际上 @tim_yates 给出了正确答案。我只想添加相应的 Spock 手册链接,解释 method unrolling 以及如何使用 where: 块中的变量影响命名。

    【讨论】:

    • 谢谢。事实上DueDateEditorTreeTableCell 的子类,其中isEmpty()final:我切换到GroovySpy 的唯一原因。我再次仔细尝试理解(并从中学习)你关于为什么这不是一个“好”测试的推理。与以往一样,在“没有测试就什么都不写”和“不要过度指定”之间存在紧张关系(对我来说,在我的低 TDD 水平上)。很抱歉没有提供 MCVE,但在这个问题的情况下,我真的只是对一件事感到困惑:使用 Unroll 时的输出。
    • 迈克,MCVE 总是有意义的。有必要运行您的代码和 IMO 对想要帮助您的人的礼貌问题。如果你寻求帮助,让他们的生活尽可能轻松。至于GroovySpy,如果DueDateEditor 是一个Java 类,它就不起作用,只有它是一个Groovy 类,因为在Java 中,Groovy 模拟/间谍的行为就像一个普通的模拟/间谍,这也被记录在案。甚至 Spock 手册(不仅仅是我)也不鼓励使用 Groovy 模拟/间谍来支持重构,并警告说“在使用它之前要三思而后行”。 IMO Groovy 模拟是一种代码味道。
    • 关于“不是一个好的测试”,我想我在回答中已经解释得足够多了。是的,测试一切,所有的代码。但是以正确的方式测试它。为什么你认为你需要测试类内部交互?取而代之的是测试方法的结果或副作用。为什么必须知道一种方法调用另一种方法?我相信您可以测试该类是否以另一种更好的方式做正确的事情。但是像往常一样,你不分享你的测试代码,所以我不能更详细地解释。
    • 请注意我关于GroovySpy 和方法展开的答案更新。
    【解决方案2】:

    不,这不是错误

    您没有告诉 spock 如何以不同的方式命名您的测试,因此它将迭代(0 然后 1)添加到名称中

    改成

    @Unroll
    def 'super start edit should be called if isEmpty for the cell returns #empty'(){
        given:
        DueDateEditor editor = GroovySpy( DueDateEditor ){
            isEmpty() >> empty
        }
    
        when:
        editor.startEdit()
    
        then:
        if( empty){
            0 * editor.callSuperStartEdit()
        }
        else {
            1 * editor.callSuperStartEdit()
        }
    
        where:
        empty << [true, false]
    }
    

    我更改了where 部分,因为只有一列的表格感觉很奇怪

    编辑

    您也可以这样做:

    @Unroll
    def 'super start edit should be called if the cell is #msg'(){
        given:
        DueDateEditor editor = GroovySpy( DueDateEditor ){
            isEmpty() >> empty
        }
    
        when:
        editor.startEdit()
    
        then:
        if( empty){
            0 * editor.callSuperStartEdit()
        }
        else {
            1 * editor.callSuperStartEdit()
        }
    
        where:
        empty << [true, false]
        msg = empty ? 'empty' : 'not empty'
    }
    

    【讨论】:

    • 谢谢。不知道你可以做一个where 这样的块。
    • 添加了一个编辑,展示了如何使标题更具描述性 ? 玩得开心!
    • 很抱歉对一个正确的答案吹毛求疵,但这两种情况都不能完全解决,因为如果你想让标题不只是描述性的,但也是正确的。否则,在两种情况之一中,标题是完全错误的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-03-02
    • 2021-10-12
    • 2013-04-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-11-29
    相关资源
    最近更新 更多