2

So I have a class:

@Service
public class MyService {

  @Autowired
  private RepositoryA repoA;

  @Autowired
  private RepositoryB repoB;

  @Transactional
  public void storeEntity(SomeEntity e) {
    repoA.save(e);

    OtherEntity o = doSomethingWithEntity(e);

    repoB.save(o);
  }
}

My method storeEntity does two saves to two different datasources. I expect that if a save to repoB fails, or doSomethingWithEntity fails, repoA.save(e) will be rollbacked.

I want to write a small tests that assures that behaviour:

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyServiceForTransactionTest {
  @Autowired
  private MyService subject;

  @Autowired
  private RepositoryA repoA;

  @MockBean
  private RepositoryB repoB;

  @Test
  public void repoBShouldNotHaveEntries() {
    // given
    when(repoB.save(any())).thenThrow(new IllegalStateException("Something wrong with db"));
    assertThat(repoB.count()).isEqualTo(0);

    // when
    SomeEntity e = ...
    subject.storeEntity(e);

    // then
    assertThat(repoA.count()).isEqualTo(0);
  }
}

This won't work, because exception is thrown and fails the test. When I surround the call with try/catch, then my assertion fails with a message that repoA has 1 entry. How to tackle this?

I also tried this:

  @Test
  public void repoBShouldNotHaveEntries() {
    // given
    when(repoB.save(any())).thenThrow(new IllegalStateException("Something wrong with db"));
    assertThat(repoB.count()).isEqualTo(0);

    // when
    SomeEntity e = ...
    try {
      subject.storeEntity(e);
    } catch (Exception e) {
      // some output here
    }
    // then
    assertThat(repoA.count()).isEqualTo(0);
  }

Assertion fails. I tried also this:

  @Test
  public void repoBShouldNotHaveEntries() {
    // given
    when(repoB.save(any())).thenThrow(new IllegalStateException("Something wrong with db"));
    assertThat(repoB.count()).isEqualTo(0);

    // when
    SomeEntity e = ...
    subject.storeEntity(e);
  }

  @After
  public void tearDown() {
    // then
    assertThat(repoA.count()).isEqualTo(0);
  }
}

Also fails. 1 record is found, but I expect that @Transactional should be rolled back.

mate00
  • 2,727
  • 5
  • 26
  • 34

1 Answers1

1

When you are managing transactions via Spring you are actually using an abstraction. For a single datasource ( typically termed as resource local transaction) transactions start/commit/rollback will work as expected but If are using two different datasources so you would need a transaction manager capable to doing distributed transactions like Bitronix or Atomikos Essentials ( in open source world). In an EE application server environment this capability is provided by the server itself. Here is a very old article worth reading ( it uses one database and one messaging broker participating in distributed transaction but the concept is same - the key is multiple resource). For a sample configuration for Bitrionix and Spring check this out.

Shailendra
  • 8,874
  • 2
  • 28
  • 37
  • Oh come on, is my case really that specific? Regular Spring configuration can't handle that? – mate00 Oct 04 '19 at 03:48
  • Spring transaction management is just an abstraction and strategy layer. For single database it delegates to the whatever mechanism your are using ( JDBC, JPA etc.) however for distributed transaction you have to tell Spring what is your transaction manager. Check this out for some more explanation https://stackoverflow.com/questions/9552718/what-is-the-difference-between-jta-and-a-local-transaction – Shailendra Oct 04 '19 at 04:58
  • Transaction coordination in case of multiple resources in not an easy job that's why you have very detailed standards X/OpenXA which describe the protocol involved. The implementation for these standards is already available in EE servers ( weblogic/Jboss/TomEE etc) so Spring does not reinvent the wheel. Also there are open source implementations available like Bitronix. Also a good artcle on this is https://www.javaworld.com/article/2077714/xa-transactions-using-spring.html – Shailendra Oct 04 '19 at 05:02