我发现本质上有两种带有线程代码的刻板印象模式:
- 面向任务–许多短期运行的同类任务,通常在Java 5执行程序框架内运行,
- 面向流程–很少,长时间运行的异构任务,通常基于事件(等待通知)或轮询(周期之间休眠),通常使用线程或可运行的方式表示。
测试这两种类型的代码可能很难。 该工作是在另一个线程中完成的,因此完成通知可能是不透明的,或者隐藏在抽象级别的后面。
该代码在GitHub上 。
提示1 –生命周期管理对象
具有生命周期受管理的对象更易于测试,该生命周期允许设置和拆卸,这意味着您可以在测试后进行清理,而没有乱码干扰任何其他测试。
public class Foo {private ExecutorService executorService;public void start() {executorService = Executors.newSingleThreadExecutor();}public void stop() {executorService.shutdown();}
}
技巧2 –设置测试超时
代码中的错误(如下所示)可能导致多线程测试永远不会完成,例如(例如)您正在等待从未设置的标志。 JUnit允许您设置测试超时。
...
@Test(timeout = 100) // in case we never get a notification
public void testGivenNewFooWhenIncrThenGetOne() throws Exception {
...
技巧3 –在与测试相同的线程中运行任务
通常,您将拥有一个在线程池中运行任务的对象。 这意味着您的单元测试可能必须等待任务完成,但是您不知道什么时候完成。 您可能会猜测,例如:
public class Foo {private final AtomicLong foo = new AtomicLong();
...public void incr() {executorService.submit(new Runnable() {@Overridepublic void run() {foo.incrementAndGet();}});}
...public long get() {return foo.get();}
}
public class FooTest {private Foo sut; // system under test@Beforepublic void setUp() throws Exception {sut = new Foo();sut.start();}@Afterpublic void tearDown() throws Exception {sut.stop();}@Testpublic void testGivenFooWhenIncrementGetOne() throws Exception {sut.incr();Thread.sleep(1000); // yuk - a slow test - don't do thisassertEquals("foo", 1, sut.get());}
}
但这是有问题的。 执行是不统一的,因此不能保证它可以在另一台机器上运行。 它很脆弱,对代码的更改可能会导致测试失败,因为它突然花费了太长时间。 它的速度很慢,因为当它失败时您会大方入睡。
一个诀窍是使任务同步运行,即与测试在同一线程中运行。 这可以通过注入执行程序来实现:
public class Foo {
...public Foo(ExecutorService executorService) {this.executorService = executorService;}
...public void stop() {// nop
}
然后,您可以使用同步执行程序服务(概念类似于SynchronousQueue)进行测试:
public class SynchronousExecutorService extends AbstractExecutorService {private boolean shutdown;@Overridepublic void shutdown() {shutdown = true;}@Overridepublic List<Runnable> shutdownNow() {shutdown = true; return Collections.emptyList();}@Overridepublic boolean isShutdown() {shutdown = true; return shutdown;}@Overridepublic boolean isTerminated() {return shutdown;}@Overridepublic boolean awaitTermination(final long timeout, final TimeUnit unit) {return true;}@Overridepublic void execute(final Runnable command) {command.run();}
}
不需要睡觉的更新测试:
public class FooTest {private Foo sut; // system under testprivate ExecutorService executorService;@Beforepublic void setUp() throws Exception {executorService = new SynchronousExecutorService();sut = new Foo(executorService);sut.start();}@Afterpublic void tearDown() throws Exception {sut.stop();executorService.shutdown();}@Testpublic void testGivenFooWhenIncrementGetOne() throws Exception {sut.incr();assertEquals("foo", 1, sut.get());}
}
请注意,您需要从外部对Foo的执行程序进行生命周期管理。
技巧4 –从线程中提取工作
如果您的线程正在等待一个事件,或者正在等待某个时间,则将其提取到自己的方法中并直接调用它。 考虑一下:
public class FooThread extends Thread {private final Object ready = new Object();private volatile boolean cancelled;private final AtomicLong foo = new AtomicLong();@Overridepublic void run() {try {synchronized (ready) {while (!cancelled) {ready.wait();foo.incrementAndGet();}}} catch (InterruptedException e) {e.printStackTrace(); // bad practise generally, but good enough for this example}}public void incr() {synchronized (ready) {ready.notifyAll();}}public long get() {return foo.get();}public void cancel() throws InterruptedException {cancelled = true;synchronized (ready) {ready.notifyAll();}}
}
而这个测试:
public class FooThreadTest {private FooThread sut;@Beforepublic void setUp() throws Exception {sut = new FooThread();sut.start();Thread.sleep(1000); // yukassertEquals("thread state", Thread.State.WAITING, sut.getState());}@Afterpublic void tearDown() throws Exception {sut.cancel();}@Afterpublic void tearDown() throws Exception {sut.cancel();}@Testpublic void testGivenNewFooWhenIncrThenGetOne() throws Exception {sut.incr();Thread.sleep(1000); // yukassertEquals("foo", 1, sut.get());}
}
现在提取工作:
@Overridepublic void run() {try {synchronized (ready) {while (!cancelled) {ready.wait();undertakeWork();}}} catch (InterruptedException e) {e.printStackTrace(); // bad practise generally, but good enough for this example}}void undertakeWork() {foo.incrementAndGet();}
重构测试:
public class FooThreadTest {private FooThread sut;@Beforepublic void setUp() throws Exception {sut = new FooThread();}@Testpublic void testGivenNewFooWhenIncrThenGetOne() throws Exception {sut.incr();sut.undertakeWork();assertEquals("foo", 1, sut.get());}
}
提示5 –通过事件通知状态更改
前面两个技巧的替代方法是使用通知系统,以便您的测试可以侦听线程对象。
这是一个面向任务的示例:
public class ObservableFoo extends Observable {private final AtomicLong foo = new AtomicLong();private ExecutorService executorService;public void start() {executorService = Executors.newSingleThreadExecutor();}public void stop() {executorService.shutdown();}public void incr() {executorService.submit(new Runnable() {@Overridepublic void run() {foo.incrementAndGet();setChanged();notifyObservers(); // lazy use of observable}});}public long get() {return foo.get();}
}
及其对应的测试(注意使用超时):
public class ObservableFooTest implements Observer {private ObservableFoo sut;private CountDownLatch updateLatch; // used to react to event@Beforepublic void setUp() throws Exception {updateLatch = new CountDownLatch(1);sut = new ObservableFoo();sut.addObserver(this);sut.start();}@Overridepublic void update(final Observable o, final Object arg) {assert o == sut;updateLatch.countDown();}@Afterpublic void tearDown() throws Exception {sut.deleteObserver(this);sut.stop();}@Test(timeout = 100) // in case we never get a notificationpublic void testGivenNewFooWhenIncrThenGetOne() throws Exception {sut.incr();updateLatch.await();assertEquals("foo", 1, sut.get());}
}
这有优点和缺点:
优点:
- 创建用于侦听对象的有用代码。
- 可以利用现有的通知代码,这使其成为已经存在的一个不错的选择。
- 更加灵活,可以同时应用于任务和面向过程的代码。
- 它比提取工作更具凝聚力。
缺点:
- 侦听器代码可能很复杂,并且会带来自己的问题,从而创建了应测试的其他生产代码。
- 将提交与通知分离。
- 要求您处理没有发送通知的情况(例如由于错误)。
- 测试代码可能很冗长,因此容易出错。
参考: Alex Collins博客博客中来自JCG合作伙伴 Alex Collins的5条关于单元测试线程代码的技巧 。
翻译自: https://www.javacodegeeks.com/2012/09/5-tips-for-unit-testing-threaded-code.html