如何在 Spock 单元测试中以 100% 的代码覆盖率测试 Groovy 构造函数

How can I test for Groovy constructors in Spock Unit test with 100% code coverage

我的问题是:我是 Spock 测试的新手,我正试图在这个用户 Class 上获得 100% 的代码覆盖率。为了让我开始,有人可以帮我弄清楚如何测试构造函数。我目前没有使用 cobertura 插件覆盖它。此外,如果有人了解 Spock + Cobertura,也许你可以阐明我做错了什么,以及一些进一步测试的指示。

我有一个代表用户的class:

import java.io.Serializable;
import java.util.Set;

class User implements Serializable {
    String email
    byte[] photo

    static hasMany = [lineups: Lineup]

    private static final long serialVersionUID = 1

    transient springSecurityService

    String username
    String password
    boolean enabled = true
    boolean accountExpired
    boolean accountLocked
    boolean passwordExpired

    User(String username, String password) {
        this()
        this.username = username
        this.password = password
    }

    @Override
    int hashCode() {
        username?.hashCode() ?: 0
    }

    @Override
    boolean equals(other) {
        is(other) || (other instanceof User && other.username == username)
    }


    Set<SecRole> getAuthorities() {
        SecUserSecRole.findAllBySecUser(this)*.secRole
    }

    def beforeInsert() {
        encodePassword()
    }

    def beforeUpdate() {
        if (isDirty('password')) {
            encodePassword()
        }
    }

    protected void encodePassword() {
        password = springSecurityService?.passwordEncoder ? springSecurityService.encodePassword(password) : password
    }

    static transients = ['springSecurityService']

    static constraints = {
        username blank: false, unique: true
        password blank: false

        email(unique: true, blank: false, nullable: true)  // needs to be moved to account
        photo(nullable:true, maxSize: 1024 * 1024 * 2 /* 2MB */)
    }

    static mapping = {
        password column: '`password`'
    }

    String toString() {
        return id + ": " + email + " " + username
    }
}

然后我有一个 Spock 单元测试:(不是我所有的代码都在这里,但只是为了我请求信息的示例...

@TestFor(User)
class UserSpec extends Specification {
    User user

    def setup() {
        user = new User(
            username: "fredflintstone",
            password: "Wilma1",
            enabled: true).save(failOnError: true, flush: true)

    }

    def cleanup() {
        user = null
    }

    // Constructor tests
    void "test to check constructor works"() {
        when:
        mockForConstraintsTests(User, [user])

        expect:
        user.validate()
        user != null
        user.username != null
        user.password != null
    }

    void "test #hashCode works"() {
        setup:

        mockForConstraintsTests(User, [user])

        expect:
        user.username.hashCode() != null

    }

}

第一个

您真的不需要(也不应该浪费时间)测试只有在 JVM 中的某些东西出现问题时才会出现问题的代码。测试基本的 getter 和 setters 对你的代码没有好处,逻辑太简单了,无法破解。因此,100% 的测试覆盖率是一个糟糕的测试目标。你应该瞄准的是...

  1. 可维护的测试
    • 他们很可能会在几个月内无人问津
    • 下一个接触他们的开发者不太可能是你
  2. 跨所有测试的一致测试格式 classes
    • 花更多的时间理解测试,更少的时间'reading'它
  3. 一种测试格式,让你一目了然地了解测试覆盖率
    • 您不需要外部工具来告诉您何时进行了足够的测试
  4. class' 方法的 100% 覆盖率(不包括具有琐碎逻辑的方法)
  5. 合理完整地覆盖每个方法的输入
    • 每个int参数至少要测试期望值的内外极值,0,负数,正数
    • 理想情况下,如果涉及多个参数,将测试所有可能的测试输入组合
    • 实际上 trim 用于测试的输入量
    • 有足够的空间

这种方法可能会导致代码覆盖工具的眼睛出现一些重叠,但它会更好地响应不断变化的条件,并让不熟悉的开发人员在尽可能短的时间内加快速度。

第二

由于您是 Spock 的新手,让我们来谈谈它。关于它的最好的事情是参数化测试是多么容易。这是我在单元测试级别测试范围的方法(功能测试并不总是那么直接)。您可能会注意到与我上面概述的测试目标有些重叠。

  • 1 规范 Class 每个 class 测试(User.class 在你的情况下)
  • 每种方法 1 次测试
    • 每个唯一的非空构造函数都算作一个方法
  • 1 测试每个可以抛出异常的方法
    • 因为异常测试逻辑通常不会补充正常测试逻辑
  • 每个输入组合 1 次测试迭代

这种方法需要对 where: 块有一个健康的理解,但有很多好处。很容易看出已经测试了哪些方法,很容易看出每个方法测试了哪些输入,它极大地鼓励了测试独立性,它鼓励代码重用,辅助方法重用可以干净且可预测地跨多个级别(单个方法的测试、整个 Spec Class、多个 Spec Classes),以及其他一些我现在想不起来的东西。


现在进入您的测试用例。如果你的构造函数除了作为一个巨大的 setter 方法之外什么都不做,你不应该花太多时间在它上面。一个确保没有发生灾难性事件的测试用例就足够了;我在这里过分地给你一个我如何编写 Spock 测试的例子。我喜欢地图列表方法,因为它使每个测试迭代紧密耦合,并允许可选变量和其他灵活性。如果你想了解更多关于 Spock 参数化测试的信息,这里是 official documentation.

@Unroll(iteration.testName)
void "testing the constructor"() {
    setup:
        user = new User(
            username: iteration.username,
            password: iteration.password,
            enabled: iteration.enabled).save(failOnError: true, flush: true) 
    when:
        mockForConstraintsTests(User, [user])

    expect:
        user != null
        user.validate() == iteration.valid
        user.username == iteration.username
        user.password == iteration.password
        user.enabled == iteration.enabled
        user.username.hashCode() != null // May need modification

    where: 
        iteration << [

        [testName: "Testing a normal enabled object"
        username: "fredflintstone"
        password: "Wilma1"
        enabled: true,
        valid: true ],

        [testName: "Testing a normal disabled object"
        username: "fredflintstone"
        password: "Wilma1"
        enabled: false,
        valid: true ],

        [testName: "Testing a disabled user, null name"
        username: null
        password: "Wilma1"
        enabled: false,
        valid: false ],

        [testName: "Testing a disabled user, empty name"
        username: ""
        password: "Wilma1"
        enabled: false,
        valid: false ],
        ]
}

如果 hashCode() 涉及的参数不止一个,那么该方法应该有自己的测试,使用 Stub 仅设置测试该方法所需的 User.class 部分。


个人意见警告!
尽管覆盖率插件很有趣,但它们会分散和掩饰典型的 jUnit 单元测试方法固有的一些主要缺陷。

如果您有 100 个测试,每个方法对应每个可能的输入排列,您的 100% 代码覆盖率分数重要吗?有点。您有一定程度的证据表明所有内容都已涵盖,但您可能无法将该覆盖范围追溯到任何单独的测试。给你一点理解力,但没有能力随时使用它。代码覆盖工具并不不好。像我上面概述的一个好的测试范围界定方法可以帮助您从测试指标中获得更多实用性。他们很容易成为不良测试方法的拐杖。