1

A simple banking application: enter image description here

Points to note:

  • Using Spring+JPA with EclipseLink as JPA provider
  • EntityManager is injected into BaseDaoImpl using @PersistenceContext
  • DAOs are autowired into the Service bean
  • @Transactional annotation used at service methods

Objective: Unit-test Service methods as part of maven build

Unit-test = simple API testing. For example, a service method: transfer(int fromAccountId, int toAccountId, double amount) has unit-test cases:

  • fromAccountId should not be 0
  • toAccountId should not be 0
  • fromAccountId != toAccountId
  • `amount is greater than 0
  • etc.

These "unit-test" cases do not require DB connection.

Problem: Build server has no DB setup. However, when the unit-test case is executed, Spring tries to connect to DB which fails. However, we do not really need DB connection for these cases to go through. (We have another set of "integration cases" - these are not executed as part of the normal build but will be executed manually with full environment available. How? - See this thread)

Questions:

  • What are the best practices to execute these kinds of unit test cases?
  • Can we force Spring not to make DB connection till the last minute it is actually needed? (Right now, it does when it encounters @Transactional method)

Adding Service Layer code as requested:

public class BankManagerImpl implements BankManager {

    @Autowired
    AccountDao accountDao;

    @Autowired
    TransactionDao transactionDao;

    ...

    @Override
    @Transactional
    public void deposit(int accountId, double amount) {
        Account a = accountDao.getAccount(accountId);
        double bal = a.getAmount();
        bal = bal + amount;
        a.setAmount(bal);

        accountDao.updateAccount(a);

        transactionDao.addTransaction(a, TransactionDao.DEPOSIT, amount);
    }

    @Override
    @Transactional
    public void withdraw(int accountId, double amount) {
        Account a = accountDao.getAccount(accountId);

        double bal = a.getAmount();
        if(bal < amount) {
            throw new RuntimeException("insufficient balance");
        }

        bal = bal - amount;
        a.setAmount(bal);

        accountDao.updateAccount(a);

        transactionDao.addTransaction(a, TransactionDao.WITHDRAW, amount);
    }

    @Override
    @Transactional
    public void transfer(int fromAccountId, int toAccountId, double amount) {
        withdraw(fromAccountId, amount);
        deposit(toAccountId, amount);
    }

    ...    

}
Community
  • 1
  • 1
gammay
  • 5,957
  • 7
  • 32
  • 51
  • 2
    What you need to completely mock the dao layer. Please show the code of the service layer – geoand Jun 24 '14 at 06:08
  • 2
    You shouldn't use Spring to unit-test a service. Simply create mock DAOs, instantiate a new BankManagerImpl with the mock DAOs as dependencies, and call the method to test. – JB Nizet Jun 24 '14 at 06:10
  • Is there a particular reason you're not using Spring Data JPA? Regardless, you should be able to simply inject a Mockito or similar mock of your DAO class for testing. – chrylis -cautiouslyoptimistic- Jun 24 '14 at 06:10
  • We do need to use Spring for unit testing and cannot directly 'new' the service bean. For instance, we have spring validations to validate API inputs. These are not triggered if it is not a spring bean. We want to include these also under unit tests. So, we tried to mock the DAO layer as suggested. The problem is, Spring makes a DB connection when it encounters `@Transactional`. So mocking out DAO does not really solve the problem. – gammay Jun 24 '14 at 08:57

3 Answers3

1

What you need is not far from an integration test. You first have to build a dummy PlatformManager and make sure it is used for you test. You will find clues for that in this other post on SO How do I mock a TransactionManager in a JUnit test, (outside of the container)? and another (partial) example below.

As spring applies ApplicationContext definition in order, the last overriding the others, you just add for you test an xml (or JavaConfig) file in last place declaring that dummy PlatformManager whith the same bean name that it has in normal config.

Then you get your service bean from the application context and replace its Dao with a mock (Mockito or what you like).

Depending of what you have to test, you will have to tweak the dummy PlatformManager, but if you simply add :

public class MockedTransactionManager implements PlatformTransactionManager {
public boolean transactionStarted = false
public commited = false;
public rollbacked = false;

@Override
public TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
    transactionStarted = true;
    return null;
}

@Override
public void commit(TransactionStatus status) throws TransactionException {
    commited = true;
}

@Override
public void rollback(TransactionStatus status) throws TransactionException {
    commited = true;
}

you will be able to control if transaction are started, commited or rollbacked. If you have special requirement, you could have to create a real SimpleTransactionStatus instead of passing a null.

Community
  • 1
  • 1
Serge Ballesta
  • 143,923
  • 11
  • 122
  • 252
  • Thank you. This is certainly one way to go. Do you know if we can mock/stub out the `PlatformTransactionManager` using `@Mock`/`@InjectMocks` annotations without having to write a new implementation? This will also avoid making changes in spring configuration only for the sake of unit testing. – gammay Jun 24 '14 at 12:52
  • Maybe it is possible, but I cannot imagine a way because the framework uses a bean declared in the application context. But you do not have to modify your config : just add another file created under test resources directory. – Serge Ballesta Jun 24 '14 at 13:32
1

The intention here is to disable DB connection during unit tests. One way to go is to Mock transaction manager as mentioned in @Serge Ballesta's answer. We found a simpler approach - we disable loading of transaction manager altogether during unit tests. This can be done by commenting out below line in application context, which prevents annotation based transactions from getting kicked in.

<!-- <tx:annotation-driven transaction-manager="transactionManager" /> -->
gammay
  • 5,957
  • 7
  • 32
  • 51
0

I would suggest that you forgo writing unit tests for the database interaction altogether.

This and this blog posts explain why.

When you are using Spring, it's so easy to just use an in-memory database in the testing environment and then proceed to write meaningful integration tests.

In your use case where you want to write a unit test for BankServiceImpl, there is no need to get Spring involved at all.

All you need to do is create that service on your own and inject the relevant mocks into it.

Writing such a test will spare you from having to mock Spring-related services (like transaction management) and will also make the test a heck of a lot quicker, since no spring context will ever need to be created.

geoand
  • 60,071
  • 24
  • 172
  • 190
  • Not using spring is not possible. I have explained why in one of the other comments. One instance is we should be able catch validation errors much earlier than in QA tests. It is cheaper to find and fix such bugs in unit testing. We use spring validation, which needs spring context to be in place. – gammay Jul 11 '14 at 13:23
  • What parts are you validating with Spring validation? Although I don't know what your code looks like, I am pretty sure that you can manually trigger Spring validation in a unit test – geoand Jul 11 '14 at 13:34
  • DTOs get validated with javax-validation annotations. It maybe possible to trigger manually - but is it worth? Why spend additional code for unit testing. In this case (with spring), the business layer gets invoked exactly the same way in a QA environment. Only, components we don't need get mocked out/disabled with simple changes in the config. – gammay Jul 11 '14 at 13:41
  • I think that it would be worth it if something like you describe comes up frequently – geoand Jul 11 '14 at 13:45