How to: Testing Spring’s @Transactional

By on 8 Feb 2018

Category: Tech matters

Tags: , ,

2 Comments

Blog home

APNIC’s software team often come up with useful solutions to programming problems that they like to share with developers. In this post, APNIC’s Jim Vella suggests that easier programming methods may be a better option than Spring’s @Transactional.

Spring’s @Transactional annotation can be simply applied to methods to make them transactional!

class MyThingService implements ThingService {
   
   @Transactional
   public void doThing(String s1, String s2) {
       ...
  }
}

But when we consider the following, things don’t seem as simple.

ThingService thingService = new MyThingService();
thingService.doThing(a, b);

In this instance thingService is not actually transactional because Spring (in its default configuration) hasn’t been afforded an opportunity to wrap thingService in a Dynamic Proxy.

Yet I’ve made this mistake, and my colleagues have made this mistake. So as diligent developers, given a production project with this bug, we want to cover this regression before implementing a fix.

How can we cover regression before implementing a fix?

We could start a Spring Context with a mock PlatformTransactionManager, excite an endpoint and look for a ‘commit’ interaction, but what about testing without Spring, especially since some folk really encourage unit tests over integration tests, as well as isolating framework dependencies to the outermost layer of an application’s architecture.

Taking a leaf from Test Driven Development, difficulty in writing a test can be a signal that the design of the application needs rethinking. An alternative way to approach transactions in Spring is programmatic transaction management.

static ThingService transactionalThingService(final ThingService thingService, final 
PlatformTransactionManager transactionManager) {
   final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
   
 return new ThingService() {
  public void doThing(String s1, String s2) {
   TransactionCallback<Void> transactionCallback = transactionStatus -> {
    thingService.doThing(s1, s2);
    return null;
   };

   transactionTemplate.execute(transactionCallback);
   }
  };
}

The factory method transactionalThingService now encompasses the concern of decorating a ThingService instance with transactional behaviour in a testable way.

@Test
public void isTransactional() {
  ThingService doNothingThingService = new ThingService() {
   @Override
   public void doThing(String s1, String s2) {
     //nothing
   }
};   


 Queue<String> transactionEvents = new LinkedList<>();
 PlatformTransactionManager monitoringTransactionManager = new PlatformTransactionManager() {
   @Override
   public TransactionStatus getTransaction(TransactionDefinition transactionDefinition) throws 
TransactionException {
      return null;
    }

    @Override
    public void commit(TransactionStatus transactionStatus) throws TransactionException {
      transactionEvents.add("committed");
    }


    @Override
    public void rollback(TransactionStatus transactionStatus) throws TransactionException {
      transactionEvents.add("rolled back");
    }
};

 ThingService transactionalThingService = transactionalThingService(doNothingThingService, 
monitoringTransactionManager);

 transactionalThingService.doThing("a", "b");

 assertThat(transactionEvents, hasItem("committed"));
}

This works well enough for ThingService, but how about in general? The purpose of using a dynamic proxy is that it can abstract over method signatures (see InvocationHandler for more details).

Object invoke(Object proxy, Method method, Object[] args)

But if we can control the design of our interfaces, this can be achieved more simply with the command object pattern:

interface ThingService extends Function<ThingService.Command, Void> {
  interface ThingService extends Function<ThingService.Command, Void> {

  class Command {
   private final String s1;
   private final String s2;

   public Command(String s1, String s2) {
    this.s1 = s1;
    this.s2 = s2;
   }

   public String getS1() {
    return s1;
   }

   public String getS2() {
    return s2;
   }
  }
 }
}

static <T, R> Function<T, R> transactionalFunction(final Function<T, R> function, final 
PlatformTransactionManager transactionManager) {
  final TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);

return (T t) -> {
  TransactionCallback<R> transactionCallback = transactionStatus -> function.apply(t);
  return transactionTemplate.execute(transactionCallback);
 };
}

Consider avoiding @Transactional

@Transactional appears simple on the surface, but is hiding complex mechanisms that can come unstuck.

The same outcome can be achieved with ordinary programming, which is also arguably more testable.

 

Rate this article

The views expressed by the authors of this blog are their own and do not necessarily reflect the views of APNIC. Please note a Code of Conduct applies to this blog.

2 Comments

  1. Markus Schreiber

    The title says `How to: Testing Spring’s @Transactional`
    and the answer of the blog is don’t use `@Transactional` but implement your own transaction handling, which is inherently stupid since you then have to care for a lot of things `@Transactional` does for you. I totally agree that this annoation has it’s quarks e.g. no default rollback for runtime exceptions, but it does care for a lot of things which you shouldn’t implement on your own as long as you don’t have to. That’s the idea behind spring anyways.

    Reply
  2. Jim Vella

    That’s right, we don’t want to implement our own transaction management. However Spring’s ‘programmatic transaction management’ is just an alternate api offered by Spring for specifying transactions. The main advantage I see in their implementation of the template pattern is that it doesn’t involve aop / dynamic proxies making it easier to test.

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *

Please answer the math question * Time limit is exhausted. Please click the refresh button next to the equation below to reload the CAPTCHA (Note: your comment will not be deleted).

Top