使用Spock在多个实现上运行规范的惯用方法是什么?

时间:2015-06-11 19:48:07

标签: spock

假设我有一个interface Joiner { String join(List<String> l); } 界面:

class Java8Joiner implements Joiner {
    @Override String join(List<String> l) { String.join("", l); }
}

class GroovyJoiner implements Joiner {
    @Override String join(List<String> l) { l.join('') }
}

它的两个实现:

class JoinerSpecification extends Specification {

    @Subject
    final def joiner = // Joiner instance here

    def "Joining #list -> #expectedString"(List<String> list, String expectedString) {

        expect:
        joiner.join(list) == expectedString

        where:
        list                | expectedString
        []                  | ''
        ['a']               | 'a'
        ['a', 'b']          | 'ab'
    }
}

检查这两个或多个实现通过相同测试的最佳方法是什么?

基本上,我想运行JoinerSpecification定义的所有测试,使用Java8Joiner,然后运行GroovyJoiner,依此类推......

<?php
session_start();
if (!isset($_SESSION["manager"])){
header("location: admin_login.php");
exit();
}
$managerID = preg_replace('#[^0-9]#i', '', $_SESSION["id"]);
$manager = preg_replace('#[^A-Za-z0-9]#i', '', $_SESSION["manager"]);
$password = preg_replace('#[^A-Za-z0-9]#i', '', $_SESSION["password"]);
include "scripts/connect.php";
$sql = mysql_query("SELECT * FROM users WHERE id='$managerID' AND username='$manager' AND password='$password' LIMIT 1");
$existCount = mysql_num_rows($sql);
if ($existCount == 0){
    echo "The login info you entered does not exist in the database.";
exit();
}
$header ="";
while ($row = mysql_fetch_array($sql)){
$name = $row["name"];
$accounttype = $row["accounttype"];
if($accounttype == "a"){
    include_once "menu_a.php";
}else{
    include_once "menu_b.php";
}
}
?>

如果需要,重构JoinerSpecification是完全没问题的。

我的主要目标是:

  • 可维护的测试代码(尽可能少的样板)
  • 清除错误消息(哪个实施失败的测试)
  • 易于从IDE运行

编辑1(2015年6月15日)

重新提出我的问题&amp;添加了一些细节以使其更清晰。

3 个答案:

答案 0 :(得分:2)

Opal的建议很有意思,但不切实际。无法使用then blocks是一个显而易见的事。

让我们尝试使用其他方法进行自我回答。由于目标是在同一接口的多个实现上执行相同的规范,为什么不尝试使用良好的旧继承呢?

abstract class AbstractJoinerSpec extends Specification {

    abstract Joiner getSubject()

    @Unroll
    def "Joining #list -> #expectedString"(List<String> list, String expectedString) {
        given:
        def joiner = getSubject()

        expect:
        joiner.join(list) == expectedString

        where:
        list                | expectedString
        []                  | ''
        ['a']               | 'a'
        ['a', 'b']          | 'ab'
    }
}


class Java8JoinerSpec extends AbstractJoinerSpec {
    @Override
    Joiner getSubject() { new Java8Joiner() }
}

class GroovyJoinerSpec extends AbstractJoinerSpec {
    @Override
    Joiner getSubject() { new Java8Joiner() }
}

基本上,唯一的抽象方法是getSubject,所有继承的测试用例都是由Spock执行的(我在文档中找不到这个功能,但确实有效)。

优点

  • 它只需要为每个实现创建一个小类,规范代码保持干净。
  • 可以使用Spock的全部功能
  • 错误消息和报告是明确的(您知道哪个实施的哪个测试失败)

<强>缺点

  • 从IDE运行给定实现的给定测试并不容易。您必须将测试用例复制/粘贴到非抽象规范中,或者删除抽象修饰符并实现getSubject方法。

我认为可以接受这种权衡,因为你不经常手动运行/调试实现。保持代码清洁,并有有意义的报告值得这样的不便。

答案 1 :(得分:1)

这里最有用的是比较链,但据我所知,它没有在groovy中实现,请参阅here。由于根据docsSubject目的是纯粹的信息,我提出了以下想法:

@Grab('org.spockframework:spock-core:0.7-groovy-2.0')
@Grab('cglib:cglib-nodep:3.1')

import spock.lang.*

class Test extends Specification {
    def 'attempt 1'() {
        given:    
        def joiners = [new GroovyJoiner(), new Java8Joiner()]

        expect:
        joiners.every { it.join(list) == expectedString }

        where:
        list                | expectedString
        []                  | ''
        ['a']               | 'a'
        ['a', 'b']          | 'ab'    
    }

    def 'attempt 2'() {
        given:    
        def gJoiner = new GroovyJoiner()
        def jJoiner = new Java8Joiner()

        expect:
        [gJoiner.join(list), jJoiner.join(list), expectedString].toSet().size() == 1

        where:
        list                | expectedString
        []                  | ''
        ['a']               | 'a'
        ['a', 'b']          | 'ab'    
    }
}

interface Joiner {
    String join(List<String> l);
}

class Java8Joiner implements Joiner {
    @Override String join(List<String> l) { String.join("", l); }
}

class GroovyJoiner implements Joiner {
    @Override String join(List<String> l) { l.join('') }
}

尝试1 中,我只是检查列表中的每个元素是否等于expectedString。它看起来不错但是在错误上它并不够冗长。任何元素都可能失败,并且没有跟踪哪一个。

尝试2 方法使用Set。由于所有结果应该相等,因此只有一个元素留在集合中。在打印测试失败的详细信息。

我的头脑中还有番石榴的ComparisonChain,但不知道它是否有用以及它的另一个项目依赖。

答案 2 :(得分:0)

不幸的是,没有办法在Spock中以声明方式创建两个列表的笛卡尔积。您必须定义自己的Iterable,它将为您的变量提供值。

如果您愿意为了可读性而牺牲简洁性,那么有一种更具可读性(使用Spock表格数据定义)的方式来定义数据。如果您感兴趣,请告诉我。否则,这里有一个解决方案,允许您以声明方式定义所有内容,而不会重复,但是您丢失了Spock的精确表格定义。

如果你的实现是无状态的,即它没有保持状态,你就可以使用Joiner的@Shared实例:

import spock.lang.Shared
import spock.lang.Specification
import spock.lang.Unroll


interface Joiner {
    String join(List<String> l);
}

class Java8Joiner implements Joiner {
    @Override
    String join(List<String> l) { String.join("", l);  }
}

class GroovyJoiner implements Joiner {
    @Override
    String join(List<String> l) { l.join('') }
}


class S1Spec extends Specification {
    @Shared
    Joiner java8Joiner = new Java8Joiner()
    @Shared
    Joiner groovyJoiner = new GroovyJoiner()

    List<Map> transpose(Map<List> mapOfLists) {
        def listOfMaps = [].withDefault { [:] }

        mapOfLists.each { k, values ->
            [values].flatten().eachWithIndex { value, index ->
                listOfMaps[index][k] = value
            }
        }
        listOfMaps
    }

    Iterable distribute(List<Joiner> joiners, Map<String, List> data) {
        def varsForEachIteration = transpose(data)

        new Iterable() {
            @Override
            Iterator iterator() {
                [joiners, varsForEachIteration].combinations().iterator()
            }
        }
    }

    @Unroll
    def "Joining with #joiner  #list -> #expectedString"() {
        expect:
        joiner.join(list) == expectedString

        where:
        [joiner, data] << distribute([java8Joiner, groovyJoiner], [
                list          : [[], ['a'], ['a', 'b']],
                expectedString: ['', 'a', 'ab']
        ])

        and:
        list = data.list
        expectedString = data.expectedString
    }

}