繁体   English   中英

随时间变化的测试,如何确保成功?

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM