DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
View Events Video Library
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Integrating PostgreSQL Databases with ANF: Join this workshop to learn how to create a PostgreSQL server using Instaclustr’s managed service

Mobile Database Essentials: Assess data needs, storage requirements, and more when leveraging databases for cloud and edge applications.

Monitoring and Observability for LLMs: Datadog and Google Cloud discuss how to achieve optimal AI model performance.

Automated Testing: The latest on architecture, TDD, and the benefits of AI and low-code tools.

Related

  • How to Migrate From JUnit 4 to JUnit 5 Step by Step
  • Creating Test Stages With JUnit
  • Guide to Scripting With the Spring Framework
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Trending

  • What You Must Know About Rate Limiting
  • Debugging Tips and Tricks: A Comprehensive Guide
  • JWT Token Revocation: Centralized Control vs. Distributed Kafka Handling
  • How To Deploy Helidon Application to Kubernetes With Kubernetes Maven Plugin
  1. DZone
  2. Coding
  3. Frameworks
  4. Unit Testing Spring Framework’s @Async Annotation

Unit Testing Spring Framework’s @Async Annotation

Mick Knutson user avatar by
Mick Knutson
·
Nov. 25, 10 · Tutorial
Like (0)
Save
Tweet
Share
26.36K Views

Join the DZone community and get the full member experience.

Join For Free

I had a task to make my current webservice calls asynchronous and wanted to use the @Async annotation in Spring 3.0x to accomplish the task.

The @Async annotation can be provided on a method so that invocation of that method will occur asynchronously. In other words, the caller will return immediately upon invocation and the actual execution of the method will occur in a task that has been submitted to a Spring TaskExecutor. In the simplest case, the annotation may be applied to a void-returning method.

This blog is going to detail how I accomplished unit testing this service.


The new @Async annotation from SpringSource version 3.0.x can be found on http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/scheduling.html section 25.5.2

The first thing that stumped me when creating this poc, is that the Object that is annotated, cannot be the came Object that is executed as a Callable<V>.

First I take my existing component I want to change from serial, to asynchonous

public interface AppointmentComponent {     public XMLGregorianCalendar getAppointmentDate(String accountNumber); }

Then the actual implmentation was this

public XMLGregorianCalendar getAppointmentDate(String accountNumber) {    logger.debug("getAppointmentDate");     XMLGregorianCalendar appointmentDate = null;     try {        AppointmentRequest request = AppointmentAssembler.assembleAppointmentRequest(accountNumber);        AppointmentResponse response = client.lookupAppointment(request);         if (response.getAppointment() != null) {            appointmentDate = response.getAppointment().getAppointmentDate();            logger.debug("found appointment date of {}.", appointmentDate);        } else {            appointmentDate = DatatypeFactory.newInstance().newXMLGregorianCalendar();            appointmentDate.setYear(BigInteger.ZERO);        }     } catch (Exception ex) {        logger.error("DAS returned an Exception ", ex.getMessage());    }    return appointmentDate;}

Then I created an Async version Interface

public interface AppointmentComponentExecutor {     public Future<XMLGregorianCalendar> getAppointmentDate(String accountNumber);     public XMLGregorianCalendar getFromFuture(Future<XMLGregorianCalendar> futureAccountInfo);     public void setAppointmentComponent(AppointmentComponent component); }

then the implementation was in 2 parts.

  1. I wanted to make the asynchronous call, and get Future<V> back to the caller.
  2. Next I wanted to provide a way to get the response object from the Future<V> and wrap the exceptions.


public class AppointmentComponentExecutorImpl implements AppointmentComponentExecutor {     @Autowired    public AppointmentComponent appointmentComponent;     // FIXME need to externalize this property.    static long timeout = 20L;     private static final Logger logger = LoggerFactory.getLogger(AppointmentComponentExecutorImpl.class);     @Async    public Future<XMLGregorianCalendar> getAppointmentDate(String accountNumber){        logger.debug("@Async getAppointmentDate(accountNumber)");        XMLGregorianCalendar appointmentDate = appointmentComponent.getAppointmentDate(accountNumber);        return new AsyncResult<XMLGregorianCalendar>(appointmentDate);    }     public XMLGregorianCalendar getFromFuture(Future<XMLGregorianCalendar> futureAccountInfo){        logger.debug("getFromFuture");         XMLGregorianCalendar appointmentDate = null;        try {            appointmentDate = futureAccountInfo.get(timeout, TimeUnit.SECONDS);        } catch (TimeoutException e) {            // TODO retry logic?        } catch (Exception e) {            logger.error("Async request returned an exception: ", e.getMessage());        }         return appointmentDate;    }     public void setAppointmentComponent(AppointmentComponent appointmentComponent){        this.appointmentComponent = appointmentComponent;    }}

I wanted to ensure I could get some form of Unit Test done, so I used JUnit and Mockito. I mocked out my component.

@RunWith(SpringJUnit4ClassRunner.class)@ContextConfiguration(locations = {"classpath:application-context-test.xml"})public class AppointmentComponentExecutorTest {     @Autowired    private AppointmentComponentExecutor componentExecutor;     private AppointmentComponent component;     protected AppointmentResponse response = new AppointmentResponse();     static final long timeout = AppointmentComponentExecutorImpl.timeout;     protected String accountNumber;     protected XMLGregorianCalendar appointmentDate;     @Before    public void setup() {        component = Mockito.mock(AppointmentComponent.class);         componentExecutor.setAppointmentComponent(this.component);        accountNumber = "1113034566667890";    }     @Test    public void testGetAppointmentDateAsync() throws Exception {         final Stopwatch stopwatch = new Stopwatch();         final XMLGregorianCalendar appointmentDate = AppointmentDateFixture                .createDateFutureAppointment();         when(component.getAppointmentDate(accountNumber))                .thenAnswer(MockUtils.getThreadDelayAnswer(appointmentDate, 20));         stopwatch.start();        long beforeAsyncCall = stopwatch.stop();         System.out.println("******* beforeAsyncCall: " + beforeAsyncCall);         Future<XMLGregorianCalendar> futureApptDate = componentExecutor                .getAppointmentDate(accountNumber);         long afterAsyncCall = stopwatch.stop();         System.out.println("******* afterAsyncCall: " + stopwatch.stop());         // Should be returned before the delayed timeout.        assertThat(futureApptDate.isDone(), is(false));         XMLGregorianCalendar dateReturned = futureApptDate.get();         long afterFutureGet = stopwatch.stop();        System.out.println("******* afterFutureGet: " + afterFutureGet);         assertThat(futureApptDate.isDone(), is(true));        verify(component).getAppointmentDate(accountNumber);         // should be the total time it takes to finish the async call.        assertThat(afterFutureGet, is(greaterThan(afterAsyncCall + 10L)));    }     @SuppressWarnings("unused")    @Test(expected = TimeoutException.class)    public void testGetAppointmentDate_Timeout() throws Exception {         // Should take 40ms        when(component.getAppointmentDate(accountNumber)).thenAnswer(                MockUtils.getThreadDelayAnswer(appointmentDate, 40));         Future<XMLGregorianCalendar> futureApptDate = componentExecutor                .getAppointmentDate(accountNumber);         XMLGregorianCalendar result = futureApptDate.get(5L, TimeUnit.MILLISECONDS);        fail();    }     @Test    public void testGetAppointmentDate_Null_Date() throws Exception {         when(component.getAppointmentDate(any(String.class)))                .thenReturn(null);         Future<XMLGregorianCalendar> futureApptDate = componentExecutor                .getAppointmentDate(accountNumber);         futureApptDate.get();     }     @Test    public void testGetFromFuture() throws Exception {         Future<XMLGregorianCalendar> future = mock(Future.class);         when(future.get(timeout, TimeUnit.SECONDS))                .thenReturn(appointmentDate);         XMLGregorianCalendar result = componentExecutor.getFromFuture(future);         verify(future).get(timeout, TimeUnit.SECONDS);     }     @Test    public void testGetFromFuture_TimeoutException() throws Exception {         Future<XMLGregorianCalendar> future = mock(Future.class);         when(future.get(timeout, TimeUnit.SECONDS))                .thenThrow(                        new TimeoutException());         XMLGregorianCalendar result = componentExecutor.getFromFuture(future);         assertNull(result);         verify(future).get(timeout, TimeUnit.SECONDS);     }    @Test    public void testGetFromFuture_ExecutionException() throws Exception {         Future<XMLGregorianCalendar> future = mock(Future.class);         // throws java.util.concurrent.ExecutionException()        when(future.get(timeout, TimeUnit.SECONDS))                .thenThrow(                        new RuntimeException());         XMLGregorianCalendar result = componentExecutor.getFromFuture(future);         assertNull(result);         verify(future).get(timeout, TimeUnit.SECONDS);     }}

Then I create aMock Helper to aid in creating a Mockito Answer delay.

mport org.mockito.invocation.InvocationOnMock;import org.mockito.stubbing.Answer; public class MockUtils {     @SuppressWarnings("rawtypes")    public static Answer getThreadDelayAnswer(final Object o, final int delay) {        return new Answer() {            public Object answer(InvocationOnMock invocation) {                try {                    Thread.sleep(delay);                } catch (InterruptedException e) {                    //ignore this error.                    //Will happen while running unit tests.                    //e.printStackTrace();                }                return o;            }        };    }}

The stopwatch implmentation is just a simple timer I have been using

public class Stopwatch {     private long start;    private long stop;     public Stopwatch() {}     public void start() {        start = System.currentTimeMillis();    }     public long stop() {        stop = System.currentTimeMillis();        return stop - start;    }}

NOTE: There is an issue with Session Scoped Beans and @Async. Here is the error:

11:31:58,917 11/04 741CE4739B8E1BC7A87CA317155100C9 ERROR IdentifyComponentImpl - DAS returned an Exception:org.springframework.remoting.RemoteAccessException: Could not access remote service at [http://pacdcdtadeva03:8050/DAS010WSWAR/services/IdentifyService]; nested exception is javax.xml.ws.WebServiceException: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.callSession': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request. 

https://jira.springframework.org/browse/SPR-6479

I will post a further blog about this issue, but not now.

Conclusion

The @Async is a very handy and easy to use annotation that hide much of the details of auto-proxying asynchronous calls. The downside, is that it hides the complexity, and when issues arise, might be difficult to track down. Also there is the mentioned issue trying to proxy requests that rely on Session scoped Objects.

 

From http://www.baselogic.com/blog/development/java-javaee-j2ee/unit-testing-springframework-s-async-annotation

Spring Framework unit test Annotation

Opinions expressed by DZone contributors are their own.

Related

  • How to Migrate From JUnit 4 to JUnit 5 Step by Step
  • Creating Test Stages With JUnit
  • Guide to Scripting With the Spring Framework
  • Java, Spring Boot, and MongoDB: Performance Analysis and Improvements

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends: