使用Mockito+Jqwik测试时方法调用次数校验异常求助
问题描述
我在用Mockito和Jqwik做属性测试,要验证chickenController.step()方法被恰好调用5次,编写了如下测试代码:
@Property void verifycoligions(@ForAll("ActionArbitrary") GUI.Action action, @ForAll boolean collidesUp,@ForAll boolean collidesDown, @ForAll boolean colidesright) { Mockito.when(chicken.getPosition()).thenReturn(new Position(0,0)); Mockito.when(chicken.getHeight()).thenReturn(40); Mockito.when(chicken.getWidth()).thenReturn(40); Mockito.when(chickenController.getLocation()).thenReturn(chicken); Mockito.when(map.collidesDown(Mockito.anyInt(), Mockito.anyInt(),Mockito.anyInt())).thenReturn(collidesDown); Mockito.when(map.collidesRight(Mockito.anyInt(), Mockito.anyInt(),Mockito.anyInt())).thenReturn(colidesright); Mockito.when(map.collidesUp(Mockito.anyInt(), Mockito.anyInt(),Mockito.anyInt())).thenReturn( collidesUp); Mockito.when(map.collidesHarmObject(Mockito.anyInt(), Mockito.anyInt(),Mockito.anyInt(),Mockito.anyInt())).thenReturn(false); mapController = new MapController(map,chickenController,offset); mapController.step(game, gui, action); Mockito.verify(chickenController, Mockito.times(5)).step(game,gui,action); }
执行测试时抛出异常:
Wanted 5 times: -> at com.chickengame.controller.MapControllerTest.verifycoligions(MapControllerTest.java:104) But was 10 times:
我以为是代码逻辑问题,把预期次数改成10后,报错变成:
Wanted 5 times: -> at com.chickengame.controller.MapControllerTest.verifycoligions(MapControllerTest.java:104) But was 5 times:
我的问题是:Mockito是否会对Mock对象的交互次数进行累加?
被测类MapController代码:
package com.chickengame.controller.game; import com.chickengame.Game; import com.chickengame.controller.Controller; import com.chickengame.gui.GUI; import com.chickengame.model.Position; import com.chickengame.model.menus.GameEndMenu; import com.chickengame.model.game.elements.Chicken; import com.chickengame.model.game.map.Map; import com.chickengame.state.menus.GameEndState; public class MapController extends Controller<Map> { private final Chicken chicken; private final ChickenController chickenController; private final int adapter; public MapController(Map location, ChickenController chickenController, int adapter) { super(location); this.chickenController = chickenController; this.chicken = chickenController.getLocation(); this.adapter = adapter; } @Override public void step(Game game, GUI gui, GUI.Action action) { Position position = chicken.getPosition(); boolean chickenOutX = (position.getX() + chicken.getWidth() <= 0); boolean chickenOutUp = (position.getY() + chicken.getHeight() <= 0); boolean chickenOutDown = (position.getY() >= 375); chickenOut(chickenOutX,chickenOutUp,chickenOutDown, game); int chickenXMin = chicken.getPosition().getX(); int chickenXMax = chickenXMin + chicken.getWidth(); int chickenYMin = chicken.getPosition().getY(); int chickenYMax = chickenYMin + chicken.getHeight(); colidesHarmObject(game, getLocation().collidesHarmObject(chickenXMin, chickenXMax, chickenYMin, chickenYMax)); for (int i = 0; i < adapter; i++) { chickenXMin = chicken.getPosition().getX(); chickenXMax = chickenXMin + chicken.getWidth(); chickenYMin = chicken.getPosition().getY(); chickenYMax = chickenYMin + chicken.getHeight(); moveCamera(-1); boolean chickenCollidesDown = (!chicken.isInverted() && getLocation().collidesDown(chickenXMin, chickenXMax, chickenYMax)); boolean chickenCollidesUp = (chicken.isInverted() && getLocation().collidesUp(chickenXMin, chickenXMax, chickenYMin)); if (getLocation().collidesHarmObject(chickenXMin, chickenXMax, chickenYMin, chickenYMax)) { break; } colidesUpDown(chickenCollidesDown, chickenCollidesUp); colidesRight(chickenXMax, chickenYMin, chickenYMax); chickenController.step(game, gui, action); } } void chickenOut(boolean chickenOutX, boolean chickenOutUp, boolean chickenOutDown, Game game ){ if(chickenOutX ||chickenOutUp ||chickenOutDown) { GameEndState endState = new GameEndState(new GameEndMenu("gameOver")); game.setState(endState); } } public void colidesHarmObject(Game game,boolean colidesharmobject) { if(colidesharmobject) { GameEndState endState = new GameEndState(new GameEndMenu("gameOver")); game.setState(endState); } } public void colidesUpDown(boolean chickenCollidesDown, boolean chickenCollidesUp) { if(chickenCollidesDown || chickenCollidesUp) { chicken.setCharge(true); } else { chickenController.moveY(); } } public void colidesRight(int chickenXMax, int chickenYMin, int chickenYMax) { if(getLocation().collidesRight(chickenXMax,chickenYMin,chickenYMax)) { chickenController.stopX(); } } public void moveCamera(int offset) { getLocation().moveMap(offset); } }
另外,用Mockito的InOrder校验时,测试能正常执行。
问题分析与解答
核心结论:Mockito会累加Mock对象的交互次数
Mockito默认不会自动重置Mock对象的交互记录——如果你的chickenController是类级别的Mock实例(比如用@Mock注解在测试类字段上),Jqwik每次执行@Property方法时,之前测试用例的交互次数会被保留,导致累加:
- 第一次测试用例执行后,
chickenController.step()被调用5次; - 第二次测试用例执行时,交互次数叠加到10次,此时验证5次就会报错;
- 当你把预期次数改成10时,可能某次测试用例只执行了一轮(调用5次),所以又出现实际次数5 vs 预期10的错误。
而InOrder校验能正常执行,是因为它只关注方法调用的顺序,不依赖累计的调用次数——只要当前测试用例中的调用顺序符合预期,就会通过,不会受到之前用例的交互记录影响。
解决方案
要解决这个问题,需要在每次属性测试用例执行前重置Mock对象的交互记录,有几种可行方式:
在测试方法开头手动重置Mock
在verifycoligions方法最开始添加:Mockito.reset(chickenController, chicken, map);确保每次测试用例执行前,清空之前的交互记录和存根设置。
使用
@BeforeEach注解统一重置
如果用JUnit 5,在测试类中添加:@BeforeEach void resetMocks() { Mockito.reset(chickenController, chicken, map); }每个测试用例执行前自动重置所有Mock。
使用
@ResetMocks注解自动重置
在测试类上添加@ResetMocks注解,每次测试后自动重置所有Mock对象:@ResetMocks class MapControllerTest { @Mock private ChickenController chickenController; // 其他Mock字段... }
另外,从被测代码来看,chickenController.step()的调用次数由adapter参数(测试中传入的offset)决定——确保offset参数固定为5,避免因参数变化导致调用次数不一致。
内容的提问来源于stack exchange,提问作者Luis Santos

