[英]Time-dependent tests, how to ensure success?
对于尚未花时间充分研究眼前问题的人来说,这可能是一个永恒的问题,但是这里...
测试是这样的:
@Test
public void expiryWorksAsExpected()
throws IOException, InterruptedException
{
final MessageSource source2 = mock(MessageSource.class);
final MessageSource source3 = mock(MessageSource.class);
when(loader.load(any(Locale.class)))
.thenReturn(source)
.thenReturn(source2)
.thenReturn(source3);
final MessageSourceProvider provider = builder.setLoader(loader)
.setExpiryTime(10L, TimeUnit.MILLISECONDS).build();
final MessageSource first = provider.getMessageSource(Locale.ROOT);
TimeUnit.MILLISECONDS.sleep(50L);
final MessageSource second = provider.getMessageSource(Locale.ROOT);
TimeUnit.MILLISECONDS.sleep(50L);
final MessageSource third = provider.getMessageSource(Locale.ROOT);
verify(loader, times(3)).load(Locale.ROOT); // HERE
assertSame(first, source);
assertSame(second, source2);
assertSame(third, source3);
}
在标记为HERE
的点,测试失败...有时(双关语)。 但是我不明白为什么。 因此,我将在此处展开代码。
首先: source
是一个mock(MessageSource.class)
(明智地定义测试类), MessageSource
代码如下:
public interface MessageSource
{
String getKey(final String key);
}
其次: loader
是一个mock(MessageSourceLoader.class)
,它是:
public interface MessageSourceLoader
{
MessageSource load(final Locale locale)
throws IOException;
}
第三: builder
是一个LoadingMessageSourceProvider.Builder
; 下面的完整代码,删除了注释(仍然很长的阅读,对此表示抱歉):
@ThreadSafe
public final class LoadingMessageSourceProvider
implements MessageSourceProvider
{
private static final ThreadFactory THREAD_FACTORY = new ThreadFactory()
{
private final ThreadFactory factory = Executors.defaultThreadFactory();
@Override
public Thread newThread(final Runnable r)
{
final Thread ret = factory.newThread(r);
ret.setDaemon(true);
return ret;
}
};
// From a custom API -- more details on demand
private static final InternalBundle BUNDLE = InternalBundle.getInstance();
private static final int NTHREADS = 3;
private final ExecutorService service
= Executors.newFixedThreadPool(NTHREADS, THREAD_FACTORY);
private final MessageSourceLoader loader;
private final MessageSource defaultSource;
private final long timeoutDuration;
private final TimeUnit timeoutUnit;
private final AtomicBoolean expiryEnabled;
private final long expiryDuration;
private final TimeUnit expiryUnit;
private final Map<Locale, FutureTask<MessageSource>> sources
= new HashMap<Locale, FutureTask<MessageSource>>();
private LoadingMessageSourceProvider(final Builder builder)
{
loader = builder.loader;
defaultSource = builder.defaultSource;
timeoutDuration = builder.timeoutDuration;
timeoutUnit = builder.timeoutUnit;
expiryDuration = builder.expiryDuration;
expiryUnit = builder.expiryUnit;
expiryEnabled = new AtomicBoolean(expiryDuration == 0L);
}
public static Builder newBuilder()
{
return new Builder();
}
@Override
public MessageSource getMessageSource(final Locale locale)
{
if (!expiryEnabled.getAndSet(true))
setupExpiry(expiryDuration, expiryUnit);
FutureTask<MessageSource> task;
synchronized (sources) {
task = sources.get(locale);
if (task == null) {
task = loadingTask(locale);
sources.put(locale, task);
service.execute(task);
}
}
try {
final MessageSource source = task.get(timeoutDuration, timeoutUnit);
return source == null ? defaultSource : source;
} catch (InterruptedException ignored) {
Thread.currentThread().interrupt();
return defaultSource;
} catch (ExecutionException ignored) {
return defaultSource;
} catch (TimeoutException ignored) {
return defaultSource;
} catch (CancellationException ignored) {
return defaultSource;
}
}
private FutureTask<MessageSource> loadingTask(final Locale locale)
{
return new FutureTask<MessageSource>(new Callable<MessageSource>()
{
@Override
public MessageSource call()
throws IOException
{
return loader.load(locale);
}
});
}
private void setupExpiry(final long duration, final TimeUnit unit)
{
final Runnable runnable = new Runnable()
{
@Override
public void run()
{
final List<FutureTask<MessageSource>> tasks;
synchronized (sources) {
tasks = new ArrayList<FutureTask<MessageSource>>(
sources.values());
sources.clear();
}
for (final FutureTask<MessageSource> task: tasks)
task.cancel(true);
}
};
// Overkill?
final ScheduledExecutorService scheduled
= Executors.newScheduledThreadPool(1, THREAD_FACTORY);
scheduled.scheduleAtFixedRate(runnable, duration, duration, unit);
}
public static final class Builder
{
private MessageSourceLoader loader;
private MessageSource defaultSource;
private long timeoutDuration = 1L;
private TimeUnit timeoutUnit = TimeUnit.SECONDS;
private long expiryDuration = 10L;
private TimeUnit expiryUnit = TimeUnit.MINUTES;
private Builder()
{
}
public Builder setLoader(final MessageSourceLoader loader)
{
BUNDLE.checkNotNull(loader, "cfg.nullLoader");
this.loader = loader;
return this;
}
public Builder setDefaultSource(final MessageSource defaultSource)
{
BUNDLE.checkNotNull(defaultSource, "cfg.nullDefaultSource");
this.defaultSource = defaultSource;
return this;
}
public Builder setLoadTimeout(final long duration, final TimeUnit unit)
{
BUNDLE.checkArgument(duration > 0L, "cfg.nonPositiveDuration");
BUNDLE.checkNotNull(unit, "cfg.nullTimeUnit");
timeoutDuration = duration;
timeoutUnit = unit;
return this;
}
public Builder setExpiryTime(final long duration, final TimeUnit unit)
{
BUNDLE.checkArgument(duration > 0L, "cfg.nonPositiveDuration");
BUNDLE.checkNotNull(unit, "cfg.nullTimeUnit");
expiryDuration = duration;
expiryUnit = unit;
return this;
}
public Builder neverExpires()
{
expiryDuration = 0L;
return this;
}
public MessageSourceProvider build()
{
BUNDLE.checkArgument(loader != null, "cfg.noLoader");
return new LoadingMessageSourceProvider(this);
}
}
}
现在的问题是:我不时看到测试失败; 更具体地说,在它检查加载程序已被正确调用过三次的行。 尽管我之前从未见过测试失败,毫秒级延迟可能太短等事实,但我还是想确保这样的测试能够成功运行-我想测试我的逻辑。 我如何做到这一点而又不求助于10毫秒的有效时间和两次提取之间的不合理睡眠(例如2秒)?
编辑该测试的目的是验证是否遵守了到期时间。 在这里,我将一个加载器设置为10毫秒的到期时间,并尝试从其第一次读取,然后暂停50毫秒,再次读取,然后暂停50毫秒,然后再次读取; 我想确保使用过期的.thenReturn()
链接的.thenReturn()
调用来确保到期有效
我将以不同的方式共同解决这个问题。 在代码中直接使用时间是错误的。 您需要的是TimeService。 然后,您的问题将很容易解决,您可以模拟TimeService。
我想我会分离出消息获取(如果我正确理解了您的问题)和过期消息的概念。 因此,有一些方法可以注入ExpirationPolicy。 在单元测试期间,您可以通过提供可以完全控制的伪造版本来离散控制。 在生产代码中,您可以使用TimedExpirationPolicy,它将在计时器上运行,您可以完全独立于此类进行测试。
您不仅要做到这一点,以使您的测试变得不那么片状,而且您更好地遵循单一责任原则。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.