You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spock Groovy测试中,赋值给实例变量的Mock对象为null异常排查

Spock测试Jenkins CpsScript Mock触发NullPointerException问题排查

问题描述

用Spock对Groovy类做单元测试时,Mock了Jenkins CI的CpsScript类,测试中MyClass的实例变量ctx突然变为null,触发NullPointerException——但在测试的when阶段创建对象时,这个变量明明不为null,看起来Mock对象的引用被莫名清空了。

示例代码

测试类

import org.jenkinsci.plugins.workflow.cps.CpsScript
import spock.lang.*

class MySpec extends Specification {

    def 'does something'() {
        given: 'valid input'
        def foo = 'foo'
       
        when: 'something happens'
        def context = createContext()
        assert context
        def obj = new MyClass(createContext())
        def success = obj.doSomething(foo)
        
        then: 'it is successful'
        assert success
    }

    private def createContext() {
        def ctx = Mock(CpsScript)
        ctx.sh(_) >> "sh called"
        return ctx
    }
}

被测试类

class MyClass {

    private final def ctx

    MyClass(ctx) {
        this.ctx = ctx
    }

    def doSomething(foo) {
        ctx.sh('// bash script that uses foo')
    }
}

问题原因

问题出在Spock Mock对象的作用域限制上:
Spock的Mock不是普通Groovy对象,是依赖测试方法上下文的动态代理。你在私有方法createContext()里创建Mock,这个方法的上下文和测试方法的闭包上下文不共享,当方法执行完毕栈帧销毁后,Spock会判定该Mock不再被测试上下文引用,直接回收它。所以当obj.doSomething()调用时,原本传入的Mock已经被销毁,ctx自然变成null。

解决方法

方法1:直接在测试方法上下文内创建Mock

把Mock的创建逻辑移到测试方法的given块里,确保Mock处于测试方法的上下文范围内:

import org.jenkinsci.plugins.workflow.cps.CpsScript
import spock.lang.*

class MySpec extends Specification {

    def 'does something'() {
        given: 'valid input'
        def foo = 'foo'
        // 在测试方法上下文内创建Mock
        def ctx = Mock(CpsScript)
        ctx.sh(_) >> "sh called"
       
        when: 'something happens'
        def obj = new MyClass(ctx)
        def success = obj.doSomething(foo)
        
        then: 'it is successful'
        success
    }
}

方法2:用setup()方法创建Mock(适合复用逻辑)

如果需要在多个测试方法里复用Mock创建逻辑,可以把Mock放在setup()方法中创建——setup()会在每个测试方法执行前运行,其上下文和测试方法共享:

import org.jenkinsci.plugins.workflow.cps.CpsScript
import spock.lang.*

class MySpec extends Specification {
    private CpsScript ctx

    def setup() {
        // setup方法的上下文和测试方法共享,Mock不会被提前回收
        ctx = Mock(CpsScript)
        ctx.sh(_) >> "sh called"
    }

    def 'does something'() {
        given: 'valid input'
        def foo = 'foo'
       
        when: 'something happens'
        def obj = new MyClass(ctx)
        def success = obj.doSomething(foo)
        
        then: 'it is successful'
        success
    }
}

注意事项

  • 不要在独立于测试方法上下文的私有方法里创建Spock Mock,避免被提前回收。
  • 尽量不要用@Shared注解标记Mock对象,会导致测试用例之间共享状态,破坏测试隔离性。

内容的提问来源于stack exchange,提问作者Boon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 18:20:59