JMockit - 期望 returns 旧值而不是与私有字段关联的值
JMockit - expectation returns old value instead of value associated with private field
当记录 returns 字段值的期望值时,我希望 returned 值是调用实际方法时字段的值(参考),而不是记录期望值时的字段值。
这是class被测(实际上是2个):
public class ListObservingCache<T> extends ObservingCache {
public ListObservingCache(Supplier<List<T>> syncFunc, int intervalMillis) {
super(syncFunc, intervalMillis);
}
@SuppressWarnings("unchecked")
@Override
public List<T> getItems() {
return items != null ? Collections.unmodifiableList((List<T>) items) : null;
}
}
public abstract class ObservingCache {
private static final int DEFAULT_CACHE_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
private static int DEFAULT_CACHE_INITIAL_DELAY = 10 * 60 * 1000; // 10 minutes
private static final int DEFAULT_THREAD_POOL_SIZE = 5;
private static ScheduledExecutorService executor;
protected Object items;
protected ObservingCache(Supplier<? extends Object> syncFunc) {
this(syncFunc, DEFAULT_CACHE_REFRESH_INTERVAL);
}
protected ObservingCache(Supplier<? extends Object> syncFunc, int intervalMillis) {
if (executor == null || executor.isShutdown()) {
executor = Executors.newScheduledThreadPool(DEFAULT_THREAD_POOL_SIZE);
}
Runnable task = () -> {
Object result = syncFunc.get();
if (result != null) {
items = result;
}
};
task.run(); // First run is blocking (saves a lot of trouble later).
executor.scheduleAtFixedRate(task, DEFAULT_CACHE_INITIAL_DELAY, intervalMillis, TimeUnit.MILLISECONDS);
}
public abstract Object getItems();
}
这是我的测试class:
public class ListObservingCacheTest {
List<Integer> provList; // <-- The field I wish to use instead of the "willReturnList()" method
@Mocked
DummyTask mockTask;
@BeforeClass
public static void setupClass() {
ObservingCache.DEFAULT_CACHE_INITIAL_DELAY = 100;
}
@AfterClass
public static void tearDownClass() {
ExecutorService toShutDown = (ExecutorService) getField(ObservingCache.class, "executor");
toShutDown.shutdown();
}
@Before
public void setUp() {
mockTask = new DummyTask(); // Empty list
}
@Test
public void testBasic() throws Exception {
willReturnList(Arrays.asList(1, 2));
ListObservingCache<Integer> obsCache = new ListObservingCache<Integer>(() -> mockTask.acquireList(), 300);
assertEquals(Arrays.asList(1, 2), obsCache.getItems());
willReturnList(Arrays.asList(3, 4, 5));
assertEquals(Arrays.asList(1, 2), obsCache.getItems()); // ObservingCache should still returns the former list because its interval hasn't passed yet
Thread.sleep(300);
assertEquals(Arrays.asList(3, 4, 5), obsCache.getItems()); // ObservingCache should now return the "new" list, as its interval has passed and the task has been executed again
}
/**
* Instructs the mock task to return the specified list when its
* acquireList() method is called
*/
private void willReturnList(List<Integer> list) {
new Expectations() {{ mockTask.acquireList(); result = list; }};
}
/**
* Simulates an ObservingCache "real-life" task. Should never really be
* called (because it's mocked).
*/
class DummyTask {
private List<Integer> list;
public List<Integer> acquireList() {
return list;
}
}
}
这个测试通过了,但是我想要一种更优雅的方式来设置对 acquireList()
方法的 return 值的期望值,因为这种 "willReturn" 方法会变成一旦我在同一个 class.
中拥有多个这样的东西,这将是一场维护噩梦
我正在寻找类似于 mockito-syntax 命令的东西:
when(mockTask.acquireList()).thenReturn(provList);
这应该始终 return provList 字段的 当前 值(与记录期望值时的值相反)。
编辑:
阅读文档后,我想出了一个解决方案,使用委托:
new Expectations() {{
mockTask.acquireList();
result = new Delegate<List<Integer>>() {
List<Integer> delegate() {
return provList; // The private field
}
};
}};
这种方法有 2 个问题:
1. 不够优雅
2. List<Integer> delegate()
方法导致编译时警告:
The method delegate() from the type new Delegate>(){} is never
used locally
因此,还在寻找其他解决方案
在代码中使用 new Expectations() 时,会创建一个具有 providedInt 值的 Expectations 实例。因此,尽管在 testRegisterInt() 中更改了 providedInt,但 Expectations 实例的状态不会更改。您可以尝试 setter 更改 Expectations 的结果。
理想情况下,存根中不应有任何逻辑。我宁愿在多个测试方法中创建多个存根(如果确实需要)或根据我的需要使用任何整数类型的东西。
OP 试图解决的问题 "solve" 是:当被测代码(这里是 obsCache.getItems()
方法)时,如何在单个测试方法中简化多个测试的编写并且要执行的验证相同,但输入值不同。
所以,这实际上是一个关于如何正确编写测试的问题。 "Arrange-Act-Assert" (AAA) 模式描述了编写良好的测试的基本形式:
@Test
public void exampleOfAAATest() {
// Arrange: set local variables/fields with input values,
// create objects and/or mocks, record expectations.
// Act: call the code to be tested; normally, this is *one* method
// call only.
// Assert: perform a number of assertions on the output, and/or
// verify expectations on mocks.
}
@Test
public void exampleOfWhatisNotAnAAATest() {
// First "test":
// Arrange 1
// Act
// Assert 1
// Second "test":
// Arrange 2 (with different inputs)
// Act again
// Assert 2
// ...
}
显然,像上面第二个这样的测试被认为是不好的做法,不应该被鼓励。
编辑:为真实 CUT 添加了完整测试 class(下方)。
public final class ListObservingCacheTest
{
@Mocked DummyTask mockTask;
final int refreshIntervalMillis = 30;
final List<Integer> initialItems = asList(1, 2);
final List<Integer> newItemsAfterRefreshInterval = asList(3, 4, 5);
@Before
public void arrangeTaskOutputForMultipleCalls() {
new Expectations() {{
mockTask.acquireList();
result = initialItems;
result = newItemsAfterRefreshInterval;
}};
// A trick to avoid a long initial delay before the scheduled task is first
// executed (a better solution might be to change the SUT to read the
// initial delay from a system property).
new MockUp<ScheduledThreadPoolExecutor>() {
@Mock
ScheduledFuture<?> scheduleAtFixedRate(
Invocation inv,
Runnable command, long initialDelay, long period, TimeUnit unit
) {
return inv.proceed(command, 0, period, unit);
}
};
}
@After
public void shutdownTheExecutorService() {
ScheduledExecutorService executorService =
Deencapsulation.getField(ObservingCache.class, ScheduledExecutorService.class);
executorService.shutdown();
}
@Test
public void getTheInitialItemsImmediatellyAfterCreatingTheCache() throws Exception {
// Arrange: empty, as there is nothing left to do beyond what the setup method
// already does.
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items = obsCache.getItems();
// Assert:
assertEquals(initialItems, items);
}
@Test
public void getTheSameItemsMultipleTimesBeforeTheCacheRefreshIntervalExpires() throws Exception {
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items1 = obsCache.getItems();
List<Integer> items2 = obsCache.getItems();
List<Integer> itemsIfTaskGotToBeCalledAgain = mockTask.acquireList();
List<Integer> items3 = obsCache.getItems();
// Assert:
assertEquals(initialItems, items1);
assertEquals(initialItems, items2);
assertEquals(initialItems, items3);
assertEquals(newItemsAfterRefreshInterval, itemsIfTaskGotToBeCalledAgain);
}
@Test
public void getNewItemsAfterTheCacheRefreshIntervalExpires() throws Exception {
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items1 = obsCache.getItems();
Thread.sleep(refreshIntervalMillis);
List<Integer> items2 = obsCache.getItems();
// Assert:
assertEquals(initialItems, items1);
assertEquals(newItemsAfterRefreshInterval, items2);
}
}
当记录 returns 字段值的期望值时,我希望 returned 值是调用实际方法时字段的值(参考),而不是记录期望值时的字段值。
这是class被测(实际上是2个):
public class ListObservingCache<T> extends ObservingCache {
public ListObservingCache(Supplier<List<T>> syncFunc, int intervalMillis) {
super(syncFunc, intervalMillis);
}
@SuppressWarnings("unchecked")
@Override
public List<T> getItems() {
return items != null ? Collections.unmodifiableList((List<T>) items) : null;
}
}
public abstract class ObservingCache {
private static final int DEFAULT_CACHE_REFRESH_INTERVAL = 10 * 60 * 1000; // 10 minutes
private static int DEFAULT_CACHE_INITIAL_DELAY = 10 * 60 * 1000; // 10 minutes
private static final int DEFAULT_THREAD_POOL_SIZE = 5;
private static ScheduledExecutorService executor;
protected Object items;
protected ObservingCache(Supplier<? extends Object> syncFunc) {
this(syncFunc, DEFAULT_CACHE_REFRESH_INTERVAL);
}
protected ObservingCache(Supplier<? extends Object> syncFunc, int intervalMillis) {
if (executor == null || executor.isShutdown()) {
executor = Executors.newScheduledThreadPool(DEFAULT_THREAD_POOL_SIZE);
}
Runnable task = () -> {
Object result = syncFunc.get();
if (result != null) {
items = result;
}
};
task.run(); // First run is blocking (saves a lot of trouble later).
executor.scheduleAtFixedRate(task, DEFAULT_CACHE_INITIAL_DELAY, intervalMillis, TimeUnit.MILLISECONDS);
}
public abstract Object getItems();
}
这是我的测试class:
public class ListObservingCacheTest {
List<Integer> provList; // <-- The field I wish to use instead of the "willReturnList()" method
@Mocked
DummyTask mockTask;
@BeforeClass
public static void setupClass() {
ObservingCache.DEFAULT_CACHE_INITIAL_DELAY = 100;
}
@AfterClass
public static void tearDownClass() {
ExecutorService toShutDown = (ExecutorService) getField(ObservingCache.class, "executor");
toShutDown.shutdown();
}
@Before
public void setUp() {
mockTask = new DummyTask(); // Empty list
}
@Test
public void testBasic() throws Exception {
willReturnList(Arrays.asList(1, 2));
ListObservingCache<Integer> obsCache = new ListObservingCache<Integer>(() -> mockTask.acquireList(), 300);
assertEquals(Arrays.asList(1, 2), obsCache.getItems());
willReturnList(Arrays.asList(3, 4, 5));
assertEquals(Arrays.asList(1, 2), obsCache.getItems()); // ObservingCache should still returns the former list because its interval hasn't passed yet
Thread.sleep(300);
assertEquals(Arrays.asList(3, 4, 5), obsCache.getItems()); // ObservingCache should now return the "new" list, as its interval has passed and the task has been executed again
}
/**
* Instructs the mock task to return the specified list when its
* acquireList() method is called
*/
private void willReturnList(List<Integer> list) {
new Expectations() {{ mockTask.acquireList(); result = list; }};
}
/**
* Simulates an ObservingCache "real-life" task. Should never really be
* called (because it's mocked).
*/
class DummyTask {
private List<Integer> list;
public List<Integer> acquireList() {
return list;
}
}
}
这个测试通过了,但是我想要一种更优雅的方式来设置对 acquireList()
方法的 return 值的期望值,因为这种 "willReturn" 方法会变成一旦我在同一个 class.
我正在寻找类似于 mockito-syntax 命令的东西:
when(mockTask.acquireList()).thenReturn(provList);
这应该始终 return provList 字段的 当前 值(与记录期望值时的值相反)。
编辑: 阅读文档后,我想出了一个解决方案,使用委托:
new Expectations() {{
mockTask.acquireList();
result = new Delegate<List<Integer>>() {
List<Integer> delegate() {
return provList; // The private field
}
};
}};
这种方法有 2 个问题:
1. 不够优雅
2. List<Integer> delegate()
方法导致编译时警告:
The method delegate() from the type new Delegate>(){} is never used locally
因此,还在寻找其他解决方案
在代码中使用 new Expectations() 时,会创建一个具有 providedInt 值的 Expectations 实例。因此,尽管在 testRegisterInt() 中更改了 providedInt,但 Expectations 实例的状态不会更改。您可以尝试 setter 更改 Expectations 的结果。
理想情况下,存根中不应有任何逻辑。我宁愿在多个测试方法中创建多个存根(如果确实需要)或根据我的需要使用任何整数类型的东西。
OP 试图解决的问题 "solve" 是:当被测代码(这里是 obsCache.getItems()
方法)时,如何在单个测试方法中简化多个测试的编写并且要执行的验证相同,但输入值不同。
所以,这实际上是一个关于如何正确编写测试的问题。 "Arrange-Act-Assert" (AAA) 模式描述了编写良好的测试的基本形式:
@Test
public void exampleOfAAATest() {
// Arrange: set local variables/fields with input values,
// create objects and/or mocks, record expectations.
// Act: call the code to be tested; normally, this is *one* method
// call only.
// Assert: perform a number of assertions on the output, and/or
// verify expectations on mocks.
}
@Test
public void exampleOfWhatisNotAnAAATest() {
// First "test":
// Arrange 1
// Act
// Assert 1
// Second "test":
// Arrange 2 (with different inputs)
// Act again
// Assert 2
// ...
}
显然,像上面第二个这样的测试被认为是不好的做法,不应该被鼓励。
编辑:为真实 CUT 添加了完整测试 class(下方)。
public final class ListObservingCacheTest
{
@Mocked DummyTask mockTask;
final int refreshIntervalMillis = 30;
final List<Integer> initialItems = asList(1, 2);
final List<Integer> newItemsAfterRefreshInterval = asList(3, 4, 5);
@Before
public void arrangeTaskOutputForMultipleCalls() {
new Expectations() {{
mockTask.acquireList();
result = initialItems;
result = newItemsAfterRefreshInterval;
}};
// A trick to avoid a long initial delay before the scheduled task is first
// executed (a better solution might be to change the SUT to read the
// initial delay from a system property).
new MockUp<ScheduledThreadPoolExecutor>() {
@Mock
ScheduledFuture<?> scheduleAtFixedRate(
Invocation inv,
Runnable command, long initialDelay, long period, TimeUnit unit
) {
return inv.proceed(command, 0, period, unit);
}
};
}
@After
public void shutdownTheExecutorService() {
ScheduledExecutorService executorService =
Deencapsulation.getField(ObservingCache.class, ScheduledExecutorService.class);
executorService.shutdown();
}
@Test
public void getTheInitialItemsImmediatellyAfterCreatingTheCache() throws Exception {
// Arrange: empty, as there is nothing left to do beyond what the setup method
// already does.
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items = obsCache.getItems();
// Assert:
assertEquals(initialItems, items);
}
@Test
public void getTheSameItemsMultipleTimesBeforeTheCacheRefreshIntervalExpires() throws Exception {
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items1 = obsCache.getItems();
List<Integer> items2 = obsCache.getItems();
List<Integer> itemsIfTaskGotToBeCalledAgain = mockTask.acquireList();
List<Integer> items3 = obsCache.getItems();
// Assert:
assertEquals(initialItems, items1);
assertEquals(initialItems, items2);
assertEquals(initialItems, items3);
assertEquals(newItemsAfterRefreshInterval, itemsIfTaskGotToBeCalledAgain);
}
@Test
public void getNewItemsAfterTheCacheRefreshIntervalExpires() throws Exception {
// Act:
ListObservingCache<Integer> obsCache =
new ListObservingCache<>(() -> mockTask.acquireList(), refreshIntervalMillis);
List<Integer> items1 = obsCache.getItems();
Thread.sleep(refreshIntervalMillis);
List<Integer> items2 = obsCache.getItems();
// Assert:
assertEquals(initialItems, items1);
assertEquals(newItemsAfterRefreshInterval, items2);
}
}