时间依赖性测试,如何确保成功?

时间:2014-02-09 01:19:32

标签: java time testng mockito

对于那些还没有花时间去全面研究手头的问题的人来说,这可能是一个永恒的问题,但这里有...

测试是这样的:

@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的点上,测试失败......不时(双关语)。但我不明白为什么。所以,我将在这里展开代码。

首先:sourcemock(MessageSource.class)(明确定义测试类),MessageSource代码如下:

public interface MessageSource
{
    String getKey(final String key);
}

第二:loadermock(MessageSourceLoader.class),即:

public interface MessageSourceLoader
{
    MessageSource load(final Locale locale)
        throws IOException;
}

第三:builderLoadingMessageSourceProvider.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 ms到期的加载器,并尝试第一次读取它,然后暂停50 ms,再次读取,然后暂停50 ms,然后第三次读取;我想确保过期有效,因为我打算使用mockito的链式.thenReturn()调用

1 个答案:

答案 0 :(得分:0)

我会以不同的方式解决这个问题。直接在代码中使用时间是错误的。你需要的是TimeService。那么你的问题将很容易解决,你可以模拟TimeService。