借助日志语句测试代码逻辑流程是否属于不良实践?
先贴出要测试的代码:
public void doSomething(int num) { var list = service.method1(num); if (!list.isEmpty()) { // Flow 1 LOG.info("List exists for {}", num); doAnotherThing(num); } else { // Flow 2 LOG.info("No list found for {}", num); } } public void doAnotherThing(int num) { Optional<Foo> optionalFoo = anotherService.get(num); optionalFoo.ifPresentOrElse( foo -> { if (!foo.type().equals("no")) { // Flow 3 anotherService.filter(foo.getFilter()); } else { // Flow 4 LOG.info("Foo is type {} - skipping", foo.type()); } }, // Flow 5 () -> LOG.info("No foo found for {} - skipping", num)); }
我的测试困境
一开始想用Mockito.verify()验证方法调用区分分支:比如测Flow1时验证anotherService.get()被调用,测Flow2时验证该方法从未被调用。但Flow4和Flow5都会调用anotherService.get()一次,且没有其他可验证的方法调用,没法用这种方式区分。
于是我写了个类在测试中捕获日志,通过检查特定日志判断进入了哪个分支。我会结合Mockito.verify()优先测可区分的分支,想请教:这种做法是不是不良实践?
我也意识到这种方式的缺点:测试依赖日志消息的准确性,稳定性差。我考虑把部分日志消息提取成受保护的静态变量,让测试代码也能引用,确保消息一致,只测试流程。如果这确实是不良实践,希望能得到测试Flow4和Flow5的具体建议。
解答
日志驱动测试不算绝对不良实践,但属于次优方案
它可以作为临时解决手段,但本质是依赖代码的**侧效应(日志输出)**而非核心业务行为,一旦日志文案修改(哪怕只是调整格式),测试就会失败,维护成本高。如果核心业务行为没有其他可观测的输出,这种方式可以用,但不要作为长期方案。你提出的提取日志常量的改进思路是可行的
把日志字符串抽到protected static final变量中,比如:protected static final String FOO_TYPE_SKIP_LOG = "Foo is type {} - skipping"; protected static final String NO_FOO_FOUND_LOG = "No foo found for {} - skipping";业务代码和测试代码都引用这些变量,能避免硬编码字符串带来的脆弱性,但依然要注意,这还是依赖侧效应,不是最理想的测试方式。
测试Flow4和Flow5的更优方案
方案一:重构代码拆分逻辑,让分支可测
把ifPresentOrElse里的两个lambda逻辑拆成独立的方法,比如:public void doAnotherThing(int num) { Optional<Foo> optionalFoo = anotherService.get(num); optionalFoo.ifPresent(this::handleExistingFoo); if (optionalFoo.isEmpty()) { handleMissingFoo(num); } } protected void handleExistingFoo(Foo foo) { if (!foo.type().equals("no")) { anotherService.filter(foo.getFilter()); } else { LOG.info(FOO_TYPE_SKIP_LOG, foo.type()); } } protected void handleMissingFoo(int num) { LOG.info(NO_FOO_FOUND_LOG, num); }测试时可以用Spy对象,验证
handleExistingFoo或handleMissingFoo是否被调用,甚至可以单独测试这两个方法的分支逻辑。方案二:结合ArgumentCaptor和返回值验证
对于Flow4:- 先mock
anotherService.get(num)返回一个type为"no"的Foo实例 - 验证
anotherService.get(num)被调用1次 - 验证
anotherService.filter(...)从未被调用(因为进入了跳过分支)
对于Flow5: - mock
anotherService.get(num)返回Optional.empty() - 验证
anotherService.get(num)被调用1次 - 同样验证
anotherService.filter(...)从未被调用
这种方式是通过验证依赖的交互行为+返回值来区分分支,比日志更可靠。
- 先mock
方案三:测试业务最终状态而非执行路径
如果这些分支最终会影响业务数据(比如数据库状态、缓存内容),优先测试最终的业务状态,而不是中间的日志或方法调用。比如Flow4是跳过过滤操作,那可以验证对应的数据没有被过滤;Flow5是没有找到Foo,那可以验证后续的业务流程没有触发对应操作。
内容的提问来源于stack exchange,提问作者sparkhee93

