Mockito(如何正确模拟嵌套对象)

Mockito (How to correctly mock nested objects)

我有下一个class:

@Service
public class BusinessService {
    @Autowired
    private RedisService redisService;
    
    private void count() {
        String redisKey = "MyKey";
        AtomicInteger counter = null;
        if (!redisService.isExist(redisKey))
            counter = new AtomicInteger(0);
        else
            counter = redisService.get(redisKey, AtomicInteger.class);

        try {
            counter.incrementAndGet();
            redisService.set(redisKey, counter, false);
            logger.info(String.format("Counter incremented by one. Current counter = %s", counter.get()));
        } catch (JsonProcessingException e) {
            logger.severe(String.format("Failed to increment counter."));
        }
    }


    // Remaining code
}

这是我的RedisService.javaclass

@Service
public class RedisService {
    private Logger logger = LoggerFactory.getLogger(RedisService.class);

    @Autowired
    private RedisConfig redisConfig;

    @PostConstruct
    public void postConstruct() {
        try {
            String redisURL = redisConfig.getUrl();
            logger.info("Connecting to Redis at " + redisURL);
            syncCommands = RedisClient.create(redisURL).connect().sync();
        } catch (Exception e) {
            logger.error("Exception connecting to Redis: " + e.getMessage(), e);
        }
    }

    
    public boolean isExist(String redisKey) {
        return syncCommands.exists(new String[] { redisKey }) == 1 ? true : false;
    }

    public <T extends Serializable> void set(String key, T object, boolean convertObjectToJson) throws JsonProcessingException {
        if (convertObjectToJson)
            syncCommands.set(key, writeValueAsString(object));
        else
            syncCommands.set(key, String.valueOf(object));
    }
    // Remaining code
}

这是我的测试class

@Mock
private RedisService redisService;

@Spy
@InjectMocks
BusinessService businessService = new BusinessService();

@Before
public void setup() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void myTest() throws Exception {
    for (int i = 0; i < 50; i++)
        Whitebox.invokeMethod(businessService, "count");
    // Remaining code
}

我的问题是当 运行 测试

时,日志中的计数器总是等于 1

Counter incremented by one. Current counter = 1(printed 50 times)

它应该打印:

Counter incremented by one. Current counter = 1

Counter incremented by one. Current counter = 2

...

...

Counter incremented by one. Current counter = 50

这意味着在每个循环内的每个方法调用中,Redis 模拟始终作为新实例传递给 BusinessService,因此我如何强制此行为成为测试方法中始终用于 Redis 的唯一实例??

注:以上代码只是为了说明我的问题的示例,并非完整代码。

您关于每次迭代都会以某种方式创建新 RedisService 的结论是错误的。

问题是它是一个模拟对象,您没有为其设置任何行为,因此它会为每个方法调用响应默认值(对象为 null,bool 为 false,int 为 0 等)。

您需要使用 Mockito.when 来设置模拟的行为。

由于以下事实导致了一些额外的复杂性:

  • 你 运行 多次循环,模拟的行为在第一次和后续迭代之间有所不同
  • 您在被测方法中创建了缓存对象。我用 doAnswer 来捕捉它。
  • 您需要使用 doAnswer().when() 而不是 when().thenAnswer 作为 set 方法 returns void
  • 最后,从 lambda 中修改了 atomicInt 变量。我把它设为 class.
  • 的字段
  • 由于每次修改 atomicInt,我再次使用 thenAnswer 而不是 thenReturn 作为 get 方法。
class BusinessServiceTest {
    @Mock
    private RedisService redisService;
    
    @InjectMocks
    BusinessService businessService = new BusinessService();

    AtomicInteger atomicInt = null;

    @BeforeEach
    public void setup() {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void myTest() throws Exception {
        // given
        Mockito.when(redisService.isExist("MyKey"))
                .thenReturn(false)
                .thenReturn(true);

        Mockito.doAnswer((Answer<Void>) invocation -> {
            atomicInt = invocation.getArgument(1);
            return null;
        }).when(redisService).set(eq("MyKey"), any(AtomicInteger.class), eq(false));

        Mockito.when(redisService.get("MyKey", AtomicInteger.class))
               .thenAnswer(invocation -> atomicInt);

        // when
        for (int i = 0; i < 50; i++) {
            Whitebox.invokeMethod(businessService, "count");
        }
        // Remaining code
    }
}

话虽如此,我仍然觉得您的代码有问题。

  • 您将 AtomicInteger 存储在 Redis 缓存中(通过将其序列化为字符串)。这个class被设计为一个进程中的多个线程使用,同一个计数器使用它的线程需要共享同一个实例。通过对其进行序列化和反序列化,您将获得(概念上)相同计数器的多个实例,在我看来,这看起来像一个错误。
  • 较小的问题:您通常不应测试私有方法
  • 2个小的:不需要实例化@InjectMocks注解的字段。你也不需要@Spy。