You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Mockito+Jqwik测试时方法调用次数校验异常求助

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对象的交互记录,有几种可行方式:

  1. 在测试方法开头手动重置Mock
    在verifycoligions方法最开始添加:

    Mockito.reset(chickenController, chicken, map);
    

    确保每次测试用例执行前,清空之前的交互记录和存根设置。

  2. 使用@BeforeEach注解统一重置
    如果用JUnit 5,在测试类中添加:

    @BeforeEach
    void resetMocks() {
        Mockito.reset(chickenController, chicken, map);
    }
    

    每个测试用例执行前自动重置所有Mock。

  3. 使用@ResetMocks注解自动重置
    在测试类上添加@ResetMocks注解,每次测试后自动重置所有Mock对象:

    @ResetMocks
    class MapControllerTest {
        @Mock
        private ChickenController chickenController;
        // 其他Mock字段...
    }
    

另外,从被测代码来看,chickenController.step()的调用次数由adapter参数(测试中传入的offset)决定——确保offset参数固定为5,避免因参数变化导致调用次数不一致。


内容的提问来源于stack exchange,提问作者Luis Santos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 10:14:55