【问题标题】:How can I test for Groovy constructors in Spock Unit test with 100% code coverage如何在具有 100% 代码覆盖率的 Spock 单元测试中测试 Groovy 构造函数
【发布时间】:2015-11-30 20:23:56
【问题描述】:

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

我有一个代表用户的类:

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

    }

}

【问题讨论】:

  • 100% 的测试覆盖率可能过于困难,而且意义不大,尤其是像 Groovy 这样可能还有其他因素在起作用的语言。一般来说,如果你能在没有太多痛苦的情况下达到 80-90%,那么你就做得很好……除此之外,你的收益可能会递减。
  • 100% 的测试覆盖率通常让我认为人们专注于实现 100%,而不是他们考虑测试的内容和原因

标签: unit-testing grails groovy spock cobertura


【解决方案1】:

首先

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

  1. 可维护的测试
    • 他们可能会几个月不被看一眼
    • 下一个接触它们的开发者不太可能是你
  2. 所有测试类的一致测试格式
    • 花更多时间理解测试,减少“阅读”时间
  3. 一种让您一目了然地了解测试覆盖率的测试格式
    • 您应该不需要外部工具来告诉您什么时候测试已经足够了
  4. 100% 覆盖类的方法(不包括具有琐碎逻辑的方法)
  5. 合理完整地覆盖每种方法的输入
    • 每个 int 参数至少应测试期望值的内极值和外极值,0、负数和正数
    • 理想情况下,如果涉及多个参数,每个可能的测试输入组合都会被测试
    • 实际上有足够的空间来减少用于测试的输入量

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

第二

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

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

这种方法需要对 where: 块有一个健康的理解,但有很多好处。很容易看到测试了哪些方法,很容易看到每个方法测试了哪些输入,它极大地鼓励了测试独立性,它鼓励代码重用,帮助方法重用可以干净且可预测地跨多个级别(单个方法的test,整个 Spec Class,多个 Spec Class),还有一些我暂时想不到的东西。


现在进入您的测试用例。如果您的构造函数除了充当一个巨大的 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% 代码覆盖率分数是否重要?有点。您有一些证据可以证明所有内容都已被覆盖,但您可能无法将该覆盖范围追溯到任何单独的测试。给你一小部分的理解,但没有能力立即使用它。代码覆盖工具并不糟糕。像我上面概述的一个好的测试范围方法可以帮助您从测试指标中获得更多实用性。它们很容易成为不良测试方法的拐杖。

【讨论】:

    猜你喜欢
    • 2023-03-21
    • 1970-01-01
    • 2013-11-28
    • 2010-10-14
    • 2014-08-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多