我正在设计软件,以便可以轻松执行单元测试。我有一个IClock
接口,除其他方法外,该接口还具有
IClock#wait(TimeUnit timeUnit, long duration)
。此方法将在 timeUnit持续时间(即1秒)内暂停当前线程。
IClock
接口有两种实现:
SimulatedClock
:具有手动增加时钟中存储时间的方法RealClock
:通过引用System.currentTimeMillis()
这是IClock#wait(...)
的默认方法:
/**
* Locks current thread for specified time
*
* @param timeUnit
* @param dt
*/
default void wait(TimeUnit timeUnit, long dt)
{
Lock lock = new ReentrantLock();
scheduleIn(timeUnit, dt, lock::unlock);
lock.lock();
}
我希望模拟单元测试的当前工作方式是
IClock#wait(...)
) SimulatedClock
时间增加一毫秒。但是,实际情况是:
IClock#wait()
,也要开始增加时间。 因此,我需要做的是确定所有线程何时完成或被阻塞。尽管可以使用Thread#getState()
完成此操作,但我宁愿采用更优雅的解决方案并与ForkJoinPool
一起使用。
package com.team2502.ezauton.utils;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class SimulatedClock implements IClock
{
private long time = 0;
private List<Job> jobs = new ArrayList<>();
public SimulatedClock() {}
public void init()
{
init(System.currentTimeMillis());
}
public void init(long time)
{
setTime(time);
}
/**
* Add time in milliseconds
*
* @param dt millisecond increase
* @return The new time
*/
public long addTime(long dt)
{
setTime(getTime() + dt);
return getTime();
}
/**
* Adds time with units
*
* @param timeUnit
* @param value
*/
public void addTime(TimeUnit timeUnit, long value)
{
addTime(timeUnit.toMillis(value));
}
/**
* Add one millisecond and returns new value
*
* @return The new time
*/
public long incAndGet()
{
return addTime(1);
}
/**
* Increment a certain amount of times
*
* @param times
*/
public void incTimes(long times, long dt)
{
long init = getTime();
long totalDt = times * dt;
for(int i = 0; i < times; i++)
{
if(!jobs.isEmpty())
{
addTime(dt);
}
else
{
break;
}
}
setTime(init + totalDt);
}
/**
* Increment a certain amount of times
*
* @param times
* @return
*/
public void incTimes(long times)
{
incTimes(times, 1);
}
@Override
public long getTime()
{
return time;
}
public void setTime(long time)
{
jobs.removeIf(job -> {
if(job.getMillis() < time)
{
job.getRunnable().run();
return true;
}
return false;
});
this.time = time;
}
@Override
public void scheduleAt(long millis, Runnable runnable)
{
if(millis < getTime())
{
throw new IllegalArgumentException("You are scheduling a task for before the current time!");
}
jobs.add(new Job(millis, runnable));
}
private static class Job
{
private final long millis;
private final Runnable runnable;
public Job(long millis, Runnable runnable)
{
this.millis = millis;
this.runnable = runnable;
}
public long getMillis()
{
return millis;
}
public Runnable getRunnable()
{
return runnable;
}
}
}
package com.team2502.ezauton.command;
import com.team2502.ezauton.utils.SimulatedClock;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.TimeUnit;
public class Simulation
{
private final SimulatedClock simulatedClock;
private List<IAction> actions = new ArrayList<>();
public Simulation()
{
simulatedClock = new SimulatedClock();
}
public SimulatedClock getSimulatedClock()
{
return simulatedClock;
}
public Simulation add(IAction action)
{
actions.add(action);
return this;
}
/**
* @param timeoutMillis Max millis
*/
public void run(long timeoutMillis)
{
simulatedClock.init();
actions.forEach(action -> new ThreadBuilder(action, simulatedClock).buildAndRun());
simulatedClock.incTimes(timeoutMillis);
// Need to wait until all threads are finished
if(!ForkJoinPool.commonPool().awaitQuiescence(1, TimeUnit.SECONDS))
{
throw new RuntimeException("Simulator did not finish in a second.");
}
}
public void run(TimeUnit timeUnit, long value)
{
run(timeUnit.toMillis(value));
}
}
@Test
public void testSimpleAction()
{
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
Simulation simulation = new Simulation();
simulation.add(new DealyedAction((TimeUnit.SECONDS, 5) -> atomicBoolean.set(true)));
simulation.run(TimeUnit.SECONDS, 100);
Assert.assertTrue(atomicBoolean.get());
}
答案 0 :(得分:0)
在调用simulatedClock.incTimes()
之前,各个线程似乎都没有及时处理。
通常在多线程测试中,一开始会有某种“约会”-允许所有线程在安全启动并运行后才能检入。如果您知道要准备多少个线程,可以使用CountDownLatch
来简化。
例如在Simulation.run()
中:
simulatedClock.init(new CountDownLatch(actions.size()));
稍后将保留对CountDownLatch
的引用。
每个线程到达SimulatedClock.scheduleAt()
时,它可以将锁存器递减一:
@Override
public void scheduleAt(long millis, Runnable runnable)
{
if(millis < getTime())
{
throw new IllegalArgumentException("You are scheduling a task for before the current time!");
}
jobs.add(new Job(millis, runnable));
countDownLatch.countDown();
}
然后incTimes()
可以等待所有线程出现:
public void incTimes(long times, long dt)
{
countDownLatch.await();
long init = getTime();
...
答案 1 :(得分:0)
我知道这不能完全回答您的问题,但是您不应该手动创建Thread
并使用现有的并发框架。
您可以这样做:
public static void main(String[] args) {
AtomicBoolean bool = new AtomicBoolean(false);
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
ScheduledFuture<?> future = executorService.schedule(() -> bool.set(true), 5, TimeUnit.SECONDS);
try {
boolean b = future.get(100, TimeUnit.SECONDS);
} catch (Exception e) {
fail();
}
assertTrue(b);
}
如果您的scheduleAt
返回Future
,您也可以将其集成到框架中。
interface Clock {
Future<?> scheduleAt(long millis, Runnable r);
}
class SchedulerService implements Clock {
ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
public Future<?> scheduleAt(long millis, Runnable r) {
Instant scheduleTime = Instant.ofEpochMilli(millis);
Instant now = Instant.now();
if (scheduleTime.isBefore(now)) {
throw new IllegalArgumentException("You are scheduling a task for before the current time!");
}
long delay = scheduleTime.minus(now).toEpochMilli();
return executorService.schedule(r, delay, TimeUnit.MILLISECONDS);
}
}
答案 2 :(得分:0)
首先,应将 IClock.wait 重命名为 IClock.sleep ,以避免混淆。
对于 RealClock ,此方法可以委托给 Thread.sleep()
对于 SimulatedClock ,此方法可以按照以下几行实现:
void sleep(TimeUnit timeUnit, long dt) {
final Object mon = new Object();
scheduleIn(timeUnit, dt,
() -> {
synchronized(mon) {
mon.notify();
}
});
synchronized(mon) {
try {
mon.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
您可以修改它以引发 InterruptedException 。
答案 3 :(得分:0)
如果您在Linux上运行,则有一个偷偷摸摸的方法。有一个实用程序(可让我不记得它的名字,而Google搜索目前无法为我找到它)可以用来为您运行程序。每当您的程序调用导致睡眠的内容时,它就会拦截该睡眠并将其替换为零长度睡眠。我认为它也会在一天中的某个时间开始撒谎,以弥补损失。
由于该方法适用于使用glibc编译的任何内容(包括JRE),因此它也适用于任何Java应用程序。
再次抱歉,无法提供有用的链接-我会继续搜寻书签/搜索,因为这听起来很有用,而我由于不记得它的名字而感到烦恼。
编辑
这样做的目的是您不必在自己的程序中有两个单独的实现。用于睡眠的标准库调用在程序的较低层被拦截。
这不是我所想到的,但它遵循相同的原则:timeskew。这似乎可以修补nanosleep()
,clock_gettime()
和select()
以及一些相关功能,因此很有可能掩盖了基础知识。