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。
我有下一个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
}
我的问题是当 运行 测试
时,日志中的计数器总是等于 1Counter 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。