如何在Spock测试中Mock方法内的对象实例化操作?
你现在遇到的问题很典型:被测的DefaultRuleParser的parseFile方法里直接new了RuleFileParserWrapper实例,导致常规的Mock方式没法替换这个内部创建的对象,没办法避免执行真实方法。你的现有测试片段只是Mock了一个对象,但被测代码完全不会用到它,所以需要调整测试方式,下面分几种场景给你解决方案:
场景1:被测类是Groovy类
如果DefaultRuleParser是Groovy编写的,Spock自带的GroovyMock可以直接拦截构造函数调用,让new RuleFileParserWrapper()返回你预先准备的Mock实例:
def "should parse file"() { setup: // 用GroovyMock全局拦截RuleFileParserWrapper的构造调用 def mockWrapper = GroovyMock(RuleFileParserWrapper, global: true) { // 预设方法返回值 parseRuleFile('filename.txt') >> [] } def parser = new DefaultRuleParser() expect: parser.parseFile('filename.txt') == [] }
这里的global: true参数是关键,它会让所有对RuleFileParserWrapper的构造函数调用都返回我们的Mock对象,这样被测方法里创建的wrapper就是我们预设好的Mock,调用parseRuleFile就会返回空列表。
场景2:被测类是Java类
Java是静态编译语言,Spock默认的Mock机制没法拦截构造函数,这时候需要借助PowerMock来修改字节码实现拦截:
首先要确保你的项目里引入了PowerMock相关依赖(比如powermock-module-junit4、powermock-api-mockito2、powermock-module-spock),然后编写测试:
@RunWith(PowerMockRunner.class) @PrepareForTest(DefaultRuleParser.class) // 指定要修改字节码的被测类 class DefaultRuleParserSpec extends Specification { def "should parse file"() { setup: def mockWrapper = Mock(RuleFileParserWrapper) // 拦截DefaultRuleParser内部的new RuleFileParserWrapper()调用 PowerMockito.whenNew(RuleFileParserWrapper.class).withNoArguments().thenReturn(mockWrapper) // 预设Mock的方法行为 mockWrapper.parseRuleFile('filename.txt') >> [] def parser = new DefaultRuleParser() expect: parser.parseFile('filename.txt') == [] } }
@PrepareForTest注解告诉PowerMock需要修改DefaultRuleParser的字节码,这样才能拦截它内部的构造函数调用;whenNew方法则指定当调用无参构造创建RuleFileParserWrapper时,返回我们的Mock实例。
更优雅的长期方案:重构代码实现依赖注入
其实从代码设计和可测试性角度,最好的方式是把RuleFileParserWrapper的实例通过构造函数注入到DefaultRuleParser里,这样完全不需要Mock构造函数,测试会更简洁:
先修改被测的DefaultRuleParser类:
public class DefaultRuleParser implements RuleParser { private final IFileParser wrapper; // 保留原有无参构造,兼容现有逻辑 public DefaultRuleParser() { this(new RuleFileParserWrapper()); } // 新增带参数的构造函数,用于测试时注入Mock public DefaultRuleParser(IFileParser wrapper) { this.wrapper = wrapper; } @Override public List<DataRule> parseFile(String filename) { return wrapper.parseRuleFile(filename); } }
然后测试就变得非常简单,直接把Mock注入即可:
def "should parse file"() { setup: def mockWrapper = Mock(RuleFileParserWrapper) // 预设Mock行为 mockWrapper.parseRuleFile('filename.txt') >> [] // 把Mock注入到被测对象中 def parser = new DefaultRuleParser(mockWrapper) expect: parser.parseFile('filename.txt') == [] }
这种方式不仅测试更简单,还让代码符合依赖倒置原则,后续如果要替换IFileParser的实现也会更方便。
内容的提问来源于stack exchange,提问作者lapots

