如何对运行在执行器服务中的代码片段进行单元测试,而不是等待Thread.sleep(time)?

30

如何对在执行器服务中运行的代码进行单元测试?在我的情况下,

public void test() {
    Runnable R = new Runnable() {
        @Override
        public void run() {
            executeTask1();
            executeTask2();
        }
    };

    ExecutorService executorService = Executors.newSingleThreadExecutor();
    executorService.submit(R);
}

当我进行单元测试时,我想要验证某个方法是否执行。

由于该方法涉及到网络操作,我将其放在一个执行器服务中运行。

在我的单元测试中,我不得不等待该方法执行完成。除了等待Thread.sleep(500)外,有更好的方法吗?

单元测试代码片段:

@Test
public void testingTask() {
    mTestObject.test();
    final long threadSleepTime = 10000L;
    Thread.sleep(threadSleepTime);
    verify(abc, times(2))
            .acquireClient(a, b, c);
    verify(abd, times(1)).addCallback(callback);
}

注意:我正在将执行器服务对象传递到此构造函数类中。我想知道是否有一种好的测试方法,而不是等待休眠时间。

10个回答

35

你也可以自己实现一个ExecutorService,它会在同一线程中运行任务。例如:

public class CurrentThreadExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}

然后您可以从AbstractExecutorService继承并使用这个实现。

如果您正在使用Guava,另一个简单的方法是使用MoreExecutors.newDirectExecutorService(),因为这样做可以避免您自己创建一个执行器服务。


1
你会建议使用这个实现来模拟ScheduledExecutorService吗? - Naman
在我的情况下,我明确地想要测试在另一个线程中运行的代码片段,因为该线程不会拥有原始线程的JPA EntityManager。只是想提一下,这并不是这种情况的解决方案。 - booFar

13
谷歌Guava提供了一个很好的类叫做MoreExecutors,当通过JUnit在并行线程中运行代码时,它帮助了我。它允许您创建Executor实例,这些实例只在同一线程中运行所有内容,本质上是真正Executor的模拟。问题在于,当在JUnit不知道的其他线程中运行时,这些Executor会使事情变得复杂,因此来自MoreExecutors的这些Executor使测试变得更加容易,因为它实际上并不是在另一个线程中运行。
请参阅MoreExecutors文档https://google.github.io/guava/releases/19.0/api/docs/com/google/common/util/concurrent/MoreExecutors.html 您可以修改类构造函数或添加一个仅在测试中使用的新构造函数,从而让您提供自己的ExecutorExecutorService。然后传入来自MoreExecutors的一个。
因此,在测试文件中,您将使用MoreExecutors创建模拟执行程序。
ExecutorService mockExecutor = MoreExecutors.newDirectExecutorService();

// or if you're using Executor instead of ExecutorService you can do MoreExecutors.newDirectExecutor()

MyService myService = new MyService(mockExecutor);

如果构造函数中没有提供Executor,则只在您的类中创建一个真正的Executor

public MyService() {}
    ExecutorService threadPool;

    public MyService(ExecutorService threadPool) {
        this.threadPool = threadPool;
    }

    public void someMethodToTest() {
        if (this.threadPool == null) {
            // if you didn't provide the executor via constructor in the unit test, 
            // it will create a real one
            threadPool = Executors.newFixedThreadPool(3);
        }
        threadPool.execute(...etc etc)
        threadPool.shutdown()
    }
}

在这里调用shutdown将有效地使该方法“阻塞”,直到Runnable完成,但您可能希望在单元测试中调用shutdown。 - rogerdpack

4
你可以使用executorService.submit(R)返回的Future实例。
来自文档:

https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ExecutorService.html#submit(java.lang.Runnable)

提交一个可运行的任务以供执行,并返回代表该任务的 Future。成功完成后,Future 的 get 方法将返回 null。
示例:
@Test
void test() throws ExecutionException {
    Future<Boolean> result = Executors.newSingleThreadExecutor().submit(() -> {
        int answer = 43;
        assertEquals(42, answer);
        return true;
    }
    assertTrue(result.get());
}

内部断言会抛出异常,导致result.get()也抛出自己的异常。因此测试将失败,并且异常的原因将告诉您为什么(“期望值为42,但实际为43”)。

1
这就是我解决它的方式,我认为这是在这里提出的答案中最好的一个。 - Nolonar
返回一个Future并调用它的get方法,或者返回一个直接的Thread对象并调用它的join方法(如果可以不使用ExecutorService),或者调用ExecutorService的shutdown方法(同样会执行join)。 - rogerdpack

1
几个选项:

  • Extract the code out of the executor service and test it 'stand alone' i.e in your example test executeTask1() and executeTask2() on their own or even together but just not by executing them in a separate thread. The fact that you are "passing an executor service object into this constructor class" helps here since you could have

    • A test which mocks the executor service and verifies that you submit the correct runnable to it
    • Test(s) which verify the behaviour of executeTask1() and executeTask2() without running them in a separate thread.
  • Use a CountDownLatch to allow your code-in-executor-service to indicate to the test thread when it is finished. For example:

    // this will be initialised and passed into the task which is run by the ExecutorService 
    // and will be decremented by that task on completion
    private CountDownLatch countdownLatch; 
    
    @Test(timeout = 1000) // just in case the latch is never decremented
    public void aTest() {
        // run your test
    
        // wait for completion
        countdownLatch.await();
    
        // assert 
        // ...
    }
    
  • Accept that you have to wait for the other thread to complete and hide the ugliness of Thread.sleep calls in your test cases by using Awaitility. For example:

    @Test
    public void aTest() {
        // run your test
    
        // wait for completion
        await().atMost(1, SECONDS).until(taskHasCompleted());
    
        // assert 
        // ...
    }
    
    private Callable<Boolean> taskHasCompleted() {
        return new Callable<Boolean>() {
            public Boolean call() throws Exception {
                // return true if your condition has been met
                return ...;
            }
        };
    }
    

1
但请注意,我的方法,比如executeTask1()和executeTask2(),都是私有方法,并且它们没有任何返回类型。我认为这种测试方式不够可靠。你能否提供一些建议,为什么这种方式是可靠的? - samuel koduri
我认为如果没有看到executeTask1()executeTask2()的实现细节,我无法提供太多关于测试它们的具体建议。如果你尝试了一些方法但是对于“我不认为这种测试方式可靠”这个问题还需要帮助的话,我也需要看到你已经尝试过什么。如果你正在尝试重构你的代码以便从我的答案中选择选项1或选项2,那么也许一个单独的问题可以解决你的具体问题,并提供一个MCVE - glytching

1

由于您控制着Executor,因此可以对其发出关闭命令:

ExecutorService myExecutorService = ...;
mTestObject = new TestObject(myExecutorService);
mTestObject.test();
myExecutorService.shutdown(); // this will wait for submitted jobs to finish, like a join
// now do asserts

即使您有一个getter,可能也是同样的情况。

如果您真的很绝望,可以使用ExecutorService的ThreadPoolExecutor实例,它具有getActiveCount()方法,并等待其降至零...


1
我同意@ejfrancis的评论,在我的情况下,我所做的是将它从局部变量移动到成员变量中,然后在测试中使用反射(可能反射不是最好的方法,但改动会更少)。
class MyClass {
            private final ExecutorService executorService = 
            Executors.newFixedThreadPool(5);;
}

那么在这里,我会在创建类之后像这样进行测试:

@BeforeEach
void init(){
   MyClass myClass = new Class();
   ReflectionTestUtils.setField(myClass, "executorService", MoreExecutors.newDirectExecutorService());
}

1
你知道“I”要大写吗?除此之外,很高兴看到ReflectionTestUtils,但控制反转仍然更好。 - Michael Piefel

0
Spring框架提供了SyncTaskExecutor,它实现了TaskExecutorExecutor接口。它可以立即执行Runnable
    @Override
    public void execute(Runnable task) {
        Assert.notNull(task, "Runnable must not be null");
        task.run();
    }

为了让所有的Spring @Service 都使用这个 Executor,您可以在 src/test/java 文件夹中提供一个 @TestConfiguration 类。
@TestConfiguration
public class TestConfig {
  
  @Bean
  public Executor syncTaskExecutor() {
    return new SyncTaskExecutor();
  }
}

@TestConfiguration 对于仅在测试执行范围内覆盖某些@Bean非常有用。根据其文档:

@Configuration可用于定义测试的其他bean或自定义项。与常规@Configuration类不同,使用@TestConfiguration不会阻止@SpringBootConfiguration的自动检测。

之后,测试类应该用@ContextConfiguration(classes = TestConfig.class)进行注释。


0

只需使用executor.awaitTermination(1, SECONDS),这将阻塞并执行等待执行的任务,之后您可以查看结果并继续测试代码,就像这个例子中一样:

@Test
fun testFileJsonStore() {
    val store = CSFileJsonStore(context, "file")
    val property: StoreTypesTestData by store.property("property")
    assertEquals(5, property.int)

    property.string = "new value"
    property.int = 123
    property.jsonObject.lateString = "new value"
    executor.awaitTermination(1, SECONDS)
    assertEquals(
        """{"property":{"key1":"new value","key2":123,"key3":{"lateStringId":"new value"}}}""",
        store.file.readString())

    val store2: CSStore = CSFileJsonStore(context, "file")
    val property2: StoreTypesTestData by store2.property("property")
    assertEquals("new value", property2.string)
    assertEquals(123, property2.int)
    assertEquals("new value", property2.jsonObject.lateString)
}

0

如果您正在使用Kotlin和mockk,那么模拟的ScheduledExecutorService是最好的选择。

class MyExecutorClass() {
    fun executeSomething() {
        Executors.newSingleThreadScheduledExecutor().schedule( {
                // do something here
            }, 3, TimeUnit.SECONDS)
    }
}
       
@Test
fun myExecutorTest() {
    // setup
    val capturingSlotRunnable: CapturingSlot<Runnable> = slot()
    mockkStatic(Executors::class)
    val mockExecutor: ScheduledExecutorService = mockk(relaxed = true)
    every { Executors.newSingleThreadScheduledExecutor() } returns mockExecutor
    every { mockExecutor.schedule(capture(capturingSlotRunnable), any(), any()) } returns null
    
    // execute
    val myExecutor = MyExecutorClass()
    myExecutor.executeSomething()
    val runnable = capturingSlotRunnable.captured
    runnable.run()

    // verify
    // TODO: verify the executable block did whatever it was supposed to
}

0

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接