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);
    }
}