如何Mock静态方法RandomStringUtils.random?Groovy+Spock测试遇阻
问题:Spock测试中无法Mock静态方法RandomStringUtils.random
环境
- Groovy 2.4
- Spock 1.3-groovy-2.4
目标代码(vars/random.groovy)
#!/usr/bin/env groovy import groovy.transform.Field import org.apache.commons.lang.RandomStringUtils @Field Random random_gen = new Random() @Field char[] charset = 'abcdefghijklmnopqrstuvwxyz1234567890'.toCharArray() String string_of_length(Integer length) { return RandomStringUtils.random(length, this.charset) } Integer bound_int(Integer lower, Integer upper) { return random_gen.nextInt() % (upper - lower) + lower } return this
失败的测试代码(test/randomSpec.groovy)
import spock.lang.Specification import groovy.lang.GroovyShell; import java.io.File; import org.apache.commons.lang.RandomStringUtils; import groovy.transform.Field /* given: Palette palette = Stub() palette.getPrimaryColour() >> Colour.Red def renderer = new Renderer(palette) expect: renderer.getForegroundColour() == Colour.Red */ @Field def random = evaluate(new File('vars/random.groovy')) class RandomSpec extends Specification { def "test string_of_length"() { given: GroovySpy(RandomStringUtils, global: true) RandomStringUtils.random(_ as int, _ as char[]) >> "abc" //def random = new GroovyShell().parse(new File('vars/random.groovy')) //File sourceFile = new File('vars/random.groovy') //def random = new GroovyClassLoader(getClass().getClassLoader()).parseClass(sourceFile).newInstance() when: def result = random.string_of_length(3) then: 1*RandomStringUtils.random(3, random.charset) "abc" == result } def "test string_of_length 2"() { given: def random = new GroovyShell().parse(new File('vars/random.groovy')) RandomStringUtils mock = Mock() when: assert "abd" == random.string_of_length(3) then: mock.random(3, 'abcdefghijklmnopqrstuvwxyz1234567890'.toCharArray()) >> "abc" } }
可行的Groovy控制台测试示例
class RandomSpec extends Specification { def "test string_of_length"() { given: GroovySpy(RandomStringUtils, global: true) def random2 = new GroovyShell().parse(new File('vars/random.groovy')) when: def result = random2.string_of_length(3) then: 1*RandomStringUtils.random(3, random2.charset) >> "abc" "abc" == result } }
失败原因分析
- 提前加载实例导致Mock失效:测试类外部的
@Field def random = evaluate(...)会在Spock初始化全局Mock之前就加载了random.groovy实例,此时该实例已经绑定了真实的RandomStringUtils静态方法,后续的GroovySpy无法拦截这个已加载实例的调用。 - Stub语法位置错误:第一个测试中,在
given块单独定义RandomStringUtils.random(_ as int, _ as char[]) >> "abc"不符合Spock规则,静态方法的Stub返回值必须和调用次数验证一起写在then块中。 - Mock对象未关联目标代码:第二个测试中创建的RandomStringUtils Mock对象完全独立,目标代码的random实例并没有使用这个Mock,仍然调用真实静态方法,所以断言失败。
正确实现方式
修正后的测试代码如下,核心要点是在Mock之后再加载目标实例,并正确使用Spock的交互验证语法:
import spock.lang.Specification import groovy.lang.GroovyShell import java.io.File import org.apache.commons.lang.RandomStringUtils class RandomSpec extends Specification { def "test string_of_length with mocked RandomStringUtils"() { given: // 先创建全局GroovySpy,拦截静态方法 GroovySpy(RandomStringUtils, global: true) // 在Mock生效后再加载random实例 def random = new GroovyShell().parse(new File('vars/random.groovy')) when: def result = random.string_of_length(3) then: // 同时验证调用次数和指定返回值 1*RandomStringUtils.random(3, random.charset) >> "abc" result == "abc" } }
额外优化建议
如果需要多次测试,可以把实例加载逻辑放到setup()方法中,但要确保setup()内先执行Mock再加载实例:
import spock.lang.Specification import groovy.lang.GroovyShell import java.io.File import org.apache.commons.lang.RandomStringUtils class RandomSpec extends Specification { def random def setup() { // 每次测试前先创建全局Mock GroovySpy(RandomStringUtils, global: true) // 再加载实例 random = new GroovyShell().parse(new File('vars/random.groovy')) } def "test string_of_length returns mocked value"() { when: def result = random.string_of_length(3) then: 1*RandomStringUtils.random(3, random.charset) >> "abc" result == "abc" } def "test string_of_length with different mocked value"() { when: def result = random.string_of_length(5) then: 1*RandomStringUtils.random(5, random.charset) >> "xyz12" result == "xyz12" } }
内容的提问来源于stack exchange,提问作者Nikolai Ehrhardt
相关产品推荐
相关产品推荐

