【问题标题】:JMockit - expectation returns old value instead of value associated with private fieldJMockit - 期望返回旧值而不是与私有字段关联的值
【发布时间】:2015-10-15 12:51:32
【问题描述】:

在记录返回字段值的期望时,我希望返回的值是调用实际方法时的字段值(引用的值),而不是期望时的字段值被记录下来了。

这是正在测试的类(实际上是其中 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();

}

这是我的测试类:

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() 方法的返回值的期望值,因为一旦我拥有多个这些在同一个班级。

我正在寻找类似于 mockito-syntax 命令的东西:

when(mockTask.acquireList()).thenReturn(provList);

这应该始终返回 provList 字段的当前值(与记录期望时的值相反)。

编辑: 在浏览了文档之后,我想出了一个使用委托的解决方案:

new Expectations() {{
                mockTask.acquireList();
                result = new Delegate<List<Integer>>() {
                    List<Integer> delegate() {
                        return provList; // The private field
                    }
                };
            }};

这种方法有两个问题:
1. 不优雅
2.List&lt;Integer&gt; delegate()方法导致编译时警告:

new Delegate>(){} 类型的方法 delegate() 永远不会 本地使用

因此,仍在寻找其他解决方案

【问题讨论】:

  • 你应该描述你想要解决的实际问题,而不是描述一个解决方案。示例测试没有帮助,因为它所做的只是测试模拟本身。
  • 我确实描述了这个问题 - 我需要找到一种优雅/有效的方法来使测试通过。提出的 3 个解决方案应该为任何试图回答的人指明方向。示例测试也是如此。我知道它测试模拟的事实。我这样写是为了尽可能以最简单的方式呈现问题。
  • 就目前的情况而言,如果您仔细想想,这个问题甚至没有意义。测试如何知道 when 设置可变字段,以便模拟方法的每次调用都返回所需的字段值?假设一个模拟 API 可以支持它,我只是看不出真实世界的测试如何使用这样的功能。
  • 模拟 API(您看似编写的)确实通过委托支持它,正如我在问题中所描述的那样。我编写的测试是我需要为真实世界的应用程序编写的真实测试的更简单版本。如果我认为有必要,我会指定真正的要求。
  • 当然,使用Delegate 允许测试返回该字段的当前值,但是谁将该字段设置为下一个值,何时设置?那是我没有得到的部分,只有在描述了要解决的实际问题后才能变得清晰。

标签: java unit-testing mocking jmockit


【解决方案1】:

OP 试图“解决”的问题是:当被测代码(这里是 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 添加了完整的测试类(如下)。

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

【讨论】:

  • 好的,那么你将如何编写这个测试呢?
  • 我会编写单独的测试。虽然,在这种情况下,被测试的方法似乎只是执行相同的代码,只是输入不同;在这种情况下,我只需执行一次,无论输入如何。
  • 在这种情况下,记录 2 个期望并具有 2 个断言是有充分理由的。我仍然认为我们不理解对方。为了清楚起见,我添加了被测类,也许这将有助于更好地理解问题。基本上,ObservingCache 应该每 300(或不同数量)毫秒运行一个 Callable 任务,并且它的 getItems() 方法应该返回该任务的结果,因此只调用它一次不会测试任何东西。 ListObservingCache 是一个 ObservingCache 扩展,它返回一个列表。
  • 我刚刚注意到 @Before 方法中的“技巧”似乎不起作用,因为原始方法仍在以 600000 的初始延迟被调用(如 ObservingCache 类中的硬编码,用调试器检查)。测试仍然通过。在我尝试使用 Mockups API 的其他地方似乎也是如此。究竟是什么触发了对被测方法的模拟?我错过了什么吗?
【解决方案2】:

当在代码中使用 new Expectations() 时,会创建一个带有 providedInt 值的 Expectations 实例。因此,尽管在 testRegisterInt() 中更改了providedInt,但Expectations 实例的状态并没有改变。你可以尝试 setter 来改变 Expectations 的结果。

理想情况下,存根中不应有任何逻辑。我宁愿在多个测试方法中创建多个存根(如果确实需要),或者根据我的需要使用 anyInteger 类型的东西。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-16
    • 1970-01-01
    • 1970-01-01
    • 2016-08-27
    • 1970-01-01
    • 2017-12-04
    • 1970-01-01
    • 2020-06-18
    相关资源
    最近更新 更多