简体   繁体   中英

JUnit-testing a Spring @Async void service method

I have a Spring service:

@Service
@Transactional
public class SomeService {

    @Async
    public void asyncMethod(Foo foo) {
        // processing takes significant time
    }
}

And I have an integration test for this SomeService :

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest
@Transactional
public class SomeServiceIntTest {

    @Inject
    private SomeService someService;

        @Test
        public void testAsyncMethod() {

            Foo testData = prepareTestData();

            someService.asyncMethod(testData);

            verifyResults();
        }

        // verifyResult() with assertions, etc.
}

Here is the problem:

  • as SomeService.asyncMethod(..) is annotated with @Async and
  • as the SpringJUnit4ClassRunner adheres to the @Async semantics

the testAsyncMethod thread will fork the call someService.asyncMethod(testData) into its own worker thread, then directly continue executing verifyResults() , possibly before the previous worker thread has finished its work.

How can I wait for someService.asyncMethod(testData) 's completion before verifying the results? Notice that the solutions to How do I write a unit test to verify async behavior using Spring 4 and annotations? don't apply here, as someService.asyncMethod(testData) returns void , not a Future<?> .

For @Async semantics to be adhered, some active @Configuration class will have the @EnableAsync annotation , eg

@Configuration
@EnableAsync
@EnableScheduling
public class AsyncConfiguration implements AsyncConfigurer {

  //

}

To resolve my issue, I introduced a new Spring profile non-async .

If the non-async profile is not active, the AsyncConfiguration is used:

@Configuration
@EnableAsync
@EnableScheduling
@Profile("!non-async")
public class AsyncConfiguration implements AsyncConfigurer {

  // this configuration will be active as long as profile "non-async" is not (!) active

}

If the non-async profile is active, the NonAsyncConfiguration is used:

@Configuration
// notice the missing @EnableAsync annotation
@EnableScheduling
@Profile("non-async")
public class NonAsyncConfiguration {

  // this configuration will be active as long as profile "non-async" is active

}

Now in the problematic JUnit test class, I explicitly activate the "non-async" profile in order to mutually exclude the async behavior:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest
@Transactional
@ActiveProfiles(profiles = "non-async")
public class SomeServiceIntTest {

    @Inject
    private SomeService someService;

        @Test
        public void testAsyncMethod() {

            Foo testData = prepareTestData();

            someService.asyncMethod(testData);

            verifyResults();
        }

        // verifyResult() with assertions, etc.
}

If you are using Mockito (directly or via Spring testing support @MockBean ), it has a verification mode with a timeout exactly for this case: https://static.javadoc.io/org.mockito/mockito-core/2.10.0/org/mockito/Mockito.html#22

someAsyncCall();
verify(mock, timeout(100)).someMethod();

You could also use Awaitility (found it on the internet, haven't tried it). https://blog.jayway.com/2014/04/23/java-8-and-assertj-support-in-awaitility-1-6-0/

someAsyncCall();
await().until( () -> assertThat(userRepo.size()).isEqualTo(1) );

I have done by injecting ThreadPoolTaskExecutor

and then

executor.getThreadPoolExecutor().awaitTermination(1, TimeUnit.SECONDS);

before verifying results, it as below:

  @Autowired
  private ThreadPoolTaskExecutor executor;

    @Test
    public void testAsyncMethod() {

        Foo testData = prepareTestData();

        someService.asyncMethod(testData);

        executor.getThreadPoolExecutor().awaitTermination(1, TimeUnit.SECONDS);

        verifyResults();
    }

In case your method returns CompletableFuture use join method - documentation CompletableFuture::join .

This method waits for the async method to finish and returns the result. Any encountered exception is rethrown in the main thread.

Just addition to the above solutions:

 @Autowired
  private ThreadPoolTaskExecutor pool;

    @Test
    public void testAsyncMethod() {
        // call async method
        someService.asyncMethod(testData);

        boolean awaitTermination = pool.getThreadPoolExecutor().awaitTermination(1, TimeUnit.SECONDS);
        assertThat(awaitTermination).isFalse();

        // verify results
    }

Just to extend the answer by @bastiat, which in my opinion should be considered the correct one, you should also specified the TaskExecutor , if you are working with multiple executors. So you would need to inject the correct one that you wish to wait for. So, let's imagine we have the following configuration class.

@Configuration
@EnableAsync
public class AsyncConfiguration {

    @Bean("myTaskExecutor")
    public TaskExecutor myTaskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setMaxPoolSize(15);
        executor.setCoreCapacity(10);
        executor.setQueueCapacity(Integer.MAX_VALUE);
        executor.setThreadNamePrefix("MyTaskExecutor-");
        executor.initialize();
        return executor;
    }

    // Everything else

}

Then, you would have a service that would look like the following one.

@Service
public class SomeServiceImplementation {

    @Async("myTaskExecutor")
    public void asyncMethod() {
         // Do something
    }

    // Everything else

}

Now, extending on @bastiat answer, the test would look like the following one.

@Autowired
private SomeService someService;

@Autowired
private ThreadPoolTaskExecutor myTaskExecutor;

@Test
public void testAsyncMethod() {

    Foo testData = prepareTestData();

    this.someService.asyncMethod(testData);

    this.myTaskExecutor.getThreadPoolExecutor().awaitTermination(1, TimeUnit.SECONDS);

    this.verifyResults();

    // Everything else
}

Also, I have a minor recommendation that has nothing to do with the question. I wouldn't add the @Transactional annotation to a service , only to the DAO/repository . Unless you need to add it to a specific service method that must be atomic .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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