Mockito如何精确校验含new PrintWriter的方法入参
Mockito校验内部new生成参数的精确匹配方案
校验失败的核心原因是:PrintWriter没有重写equals()方法,Mockito默认的参数比对逻辑会校验两个对象是否为同一个内存实例,测试代码里new的PrintWriter和业务代码里new的PrintWriter是完全独立的两个对象,自然比对失败。
可以通过以下方案实现精确校验:
方案1:使用ArgumentCaptor+自定义校验(无需修改业务代码)
通过参数捕获器拿到业务代码实际传入的PrintWriter实例,再校验它绑定的文件路径是否符合预期;第二个参数因为List默认实现了按内容比对的equals()方法,直接用eq()包裹预期值即可做精确匹配。
示例代码:
import org.mockito.ArgumentCaptor; import java.io.PrintWriter; import java.io.FileOutputStream; import java.lang.reflect.Field; import java.util.List; // 1. 定义预期值 String expectedCsvPath = ".\\xyz.csv"; List expectedList = TestUtil.getSomeList(); // 2. 执行业务测试逻辑 // 此处替换为实际调用被测方法的代码,比如xxxService.exportCsv(); // 3. 校验方法调用次数,同时捕获PrintWriter参数 ArgumentCaptor<PrintWriter> writerCaptor = ArgumentCaptor.forClass(PrintWriter.class); verify(csvWriter, times(1)).writeCsv(writerCaptor.capture(), eq(expectedList)); // 4. 校验捕获到的PrintWriter绑定的路径是否正确 PrintWriter actualWriter = writerCaptor.getValue(); Field outField = PrintWriter.class.getDeclaredField("out"); outField.setAccessible(true); Object innerObj = outField.get(actualWriter); String actualPath = null; // 逐层拆包PrintWriter内部包装的流对象,拿到最终的文件路径 while (innerObj != null) { if (innerObj instanceof FileOutputStream) { Field pathField = FileOutputStream.class.getDeclaredField("path"); pathField.setAccessible(true); actualPath = (String) pathField.get(innerObj); break; } Field nextOut = innerObj.getClass().getDeclaredField("out"); nextOut.setAccessible(true); innerObj = nextOut.get(innerObj); } // 断言路径匹配 assertEquals(expectedCsvPath, actualPath);
方案2:使用argThat自定义参数匹配器(无需修改业务代码)
不需要单独捕获参数,直接在verify语句中自定义PrintWriter的匹配规则,匹配时校验实例绑定的文件路径即可:
String expectedCsvPath = ".\\xyz.csv"; List expectedList = TestUtil.getSomeList(); verify(csvWriter, times(1)).writeCsv( argThat(writer -> { try { Field outField = PrintWriter.class.getDeclaredField("out"); outField.setAccessible(true); Object innerObj = outField.get(writer); while (innerObj != null) { if (innerObj instanceof FileOutputStream) { Field pathField = FileOutputStream.class.getDeclaredField("path"); pathField.setAccessible(true); return expectedCsvPath.equals(pathField.get(innerObj)); } Field nextOut = innerObj.getClass().getDeclaredField("out"); nextOut.setAccessible(true); innerObj = nextOut.get(innerObj); } } catch (Exception e) { return false; } return false; }), eq(expectedList) );
更推荐的可测试性优化方案(从根源解决问题)
上面两种方案都用到了反射访问JDK内部字段,不同JDK版本如果修改了流内部的字段名,测试代码就会失效。更好的做法是调整业务代码结构,把PrintWriter的创建逻辑抽离成可注入的工厂类:
- 定义工厂接口
PrintWriterFactory,包含方法PrintWriter create(String filePath) - 业务类中注入
PrintWriterFactory实例,生成PrintWriter时调用factory.create(csvPath),而不是直接new - 单元测试中直接mock
PrintWriterFactory,就可以直接校验create方法接收到的路径参数是否为.\xyz.csv,不需要再反射拆包校验PrintWriter内部状态,代码稳定性更高。
注意:Mockito的参数匹配规则要求,只要某一个参数用了匹配器(比如argThat、capture、any),其余所有参数都必须用匹配器包裹,所以第二个参数的预期列表必须用
eq()包裹,不能直接传入对象,否则会抛出参数匹配异常。
内容的提问来源于stack exchange,提问作者Sammy Pawar
相关产品推荐
相关产品推荐

