Integration tests on negative scenarios – Mockito, Dynamic Proxy, JUnit, Spring

Integration tests on negative scenarios – Mockito, Dynamic Proxy, JUnit, Spring

While writting one of the applications that I’ve recently co-created, it turned out to be necessary to check the behaviour of the app. I mean situations like database exception, infrastructure damage or network failure. Proper behaviour can be understood as database transaction rollback or compensation of already performed actions. After designing and implementing integration tests, I decided to share my experience with you

In order to explain the title problem better, I shall start by answering the question of when and why to test negative cases. Then I’ll write a simple bank application to show you how to implement integration tests in two different ways, both of which simulate web service failure. So let’s start.

1. Why to test negative scenarios?

I think that testing positive paths is quite obvious. On the one hand, we can easily perform regression tests and on the other hand, these scenarios document business cases. So what’s the point of writing negative scenarios?

We can divide negative scenarios into two groups. The first one includes negative scenarios predicted by the business like for example lack of funds during cash withdrawal.

The second group are exceptions associated with application architecture and infrastructure. I mean for example test of database transaction rollback after web service failure. It is this group that I’d like to focus on in the next part of the article.

2. Case study: Bank application

Imagine that you are writing an application for banking system responsible for operations on accounts. It provides services like payment and withdrawal, checking the balance and money transfers.

The application was divided into two components InternalBankService and BankService. For the sake of the example, let’s assume that the InternalBankService#addToAccount method behaves like a web service. The BankService component implements methods which consists of services from the InternalBankService component. These compositions have been highlighted on the listing below

package com.bettercoding.lab;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

@SpringBootApplication
public class BankApp {
    public static void main(String[] args) {
        SpringApplication.run(BankApp.class, args);
    }

    @Component
    public static class InternalBankService {
        private Map<String, BigDecimal> accounts = new HashMap<>();

        void addToAccount(String accountNumber, BigDecimal amount) {
            BigDecimal newAmount = accounts.getOrDefault(accountNumber, BigDecimal.ZERO).add(amount);
            accounts.put(accountNumber, newAmount);
        }

        BigDecimal getAmount(String accountNumber) {
            return accounts.getOrDefault(accountNumber, BigDecimal.ZERO);
        }
    }

    @Component
    public static class BankService {
        private final InternalBankService internalBankService;

        @Autowired
        public BankService(InternalBankService internalBankService) {
            this.internalBankService = internalBankService;
        }

        public void credit(String accountNumber, BigDecimal amount) {
            this.internalBankService.addToAccount(accountNumber, amount);
        }


        public void debit(String accountNumber, BigDecimal amount) {
            this.internalBankService.addToAccount(accountNumber, amount.negate());
        }

        public void transfer(String debitAccount, String creditAccount, BigDecimal amount) {
            this.internalBankService.addToAccount(creditAccount, amount);
            this.internalBankService.addToAccount(debitAccount, amount.negate());
        }

        public BigDecimal getAmount(String accountNumber) {
            return this.internalBankService.getAmount(accountNumber);
        }
    }
}

Below you can find a sample integration test of a positive scenario.

package com.bettercoding.lab;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;


@SpringBootTest
class BankAppTest {
    @Autowired
    private BankApp.BankService bankService;

    @Test
    void happyPathTest() {
        bankService.credit("account_1", BigDecimal.valueOf(100));
        bankService.credit("account_1", BigDecimal.valueOf(50));
        bankService.credit("account_2", BigDecimal.valueOf(200));
        bankService.credit("account_2", BigDecimal.valueOf(300));

        assertEquals(150, bankService.getAmount("account_1").longValue());
        assertEquals(500, bankService.getAmount("account_2").longValue());
    }
}

3. Testing negative scenarios

Writing negative scenarios is much more difficult than testing positive paths. Let’s stop for a moment and think, how to simulate different failures.

3.1. Failure simulation

It would be good to be able to control exactly when and, under what conditions, the exception will happen. In that case, how to control and simulate the failure of a component? Well, the most common symptom of failure is to get some exception from the RuntimeException group. Therefore, the simplest and very well-performing way to simulate this is to throw some kind of RuntimeException at the right moment. The next question is about how to control when the exception occurs.

In our example application, the appropriate place for such simulation will be the InternalBankService component because it implements the network services used by BankService. In the further part of the article I will present you two methods that allow you to change the behaviour of the InternalBankService component in a way that you can control its response to subsequent calls of the addToAccount method.

Both methods presented below are based on a similar idea. The first one is written from scratch using a dynamic class proxy. The second one uses the Mockito framework. I hope that it will be easier to form an opinion on the subject – is it worth to write such mechanisms from the scratch, or use ready to use tools for this purpose.

3.1.1. Method#1: Using dynamic class proxy

If the topic of dynamic class proxying is new to you or you would like to refresh your knowledge about it, I strongly encourage you to read my previous entry.

As I mentioned earlier, we would like to have control over calling the InternalBankService#addToAccount method. To make this possible, we can create a dynamic proxy of the InternalBankService class, in which we implement the mechanism of triggering the RuntimeException exception. The proxy class prepared in this way can be replaced in the context of Spring by preparing the appropriate test configuration marked with the @TestConfiguration annotation. It is important that the overridden bean has the same name as the original one. In our case it will be: public BankApp.InternalBankService internalBankService(). Another important thing is using the @DirtiesContext annotation and the DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD configuration. It causes that after each test the Spring context is reset, which also results in proxy reset.

Below you can find a sample integration test of a negative scenario. The necessary code changes has been highlighted.

package com.bettercoding.lab;

import javassist.util.proxy.ProxyFactory;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.annotation.DirtiesContext;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import static org.junit.jupiter.api.Assertions.assertEquals;


@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
class BankAppDynamicProxyTest {

    @Autowired
    private BankApp.BankService bankService;

    @Test
    void happyPathTest() {
        bankService.credit("account_1", BigDecimal.valueOf(100));
        bankService.credit("account_1", BigDecimal.valueOf(50));
        bankService.credit("account_2", BigDecimal.valueOf(200));
        bankService.credit("account_2", BigDecimal.valueOf(300));

        assertEquals(150, bankService.getAmount("account_1").longValue());
        assertEquals(500, bankService.getAmount("account_2").longValue());
    }

    @Test
    void negativePathTest() {
        TestConfig.forceExceptionDuringInvocation("addToAccount", 4);

        Assertions.assertThrows(RuntimeException.class, () -> {
            bankService.transfer("account_1", "account_2", BigDecimal.valueOf(100));
            bankService.transfer("account_1", "account_2", BigDecimal.valueOf(50));
        });


        assertEquals(-100, bankService.getAmount("account_1").longValue());
        assertEquals(100, bankService.getAmount("account_2").longValue());
    }

    @TestConfiguration
    public static class TestConfig {
        private static Map<String, AtomicInteger> INTERNAL_BANK_SERVICE_METHOD_MAP = new HashMap<>();

        public static void forceExceptionDuringInvocation(String methodName, int invocationsNo) {
            INTERNAL_BANK_SERVICE_METHOD_MAP.put(methodName, new AtomicInteger(invocationsNo));
        }

        @Bean
        public BankApp.InternalBankService internalBankService() {
            ProxyFactory proxyFactory = new ProxyFactory();
            proxyFactory.setSuperclass(BankApp.InternalBankService.class);

            try {
                return (BankApp.InternalBankService) proxyFactory.create(new Class<?>[0], new Object[0], (self, thisMethod, proceed, args) -> {
                    System.out.println(">> Mock called before original method invocation: " + thisMethod.getName());

                    int value = INTERNAL_BANK_SERVICE_METHOD_MAP.getOrDefault(thisMethod.getName(), new AtomicInteger(Integer.MAX_VALUE)).decrementAndGet();
                    if (value <= 0) {
                        INTERNAL_BANK_SERVICE_METHOD_MAP.remove(thisMethod.getName());
                        throw new RuntimeException("Triggered exception");
                    }

                    Object result = proceed.invoke(self, args);


                    System.out.println("<< Mock called after original method invocation:" + thisMethod.getName());
                    return result;
                });
            } catch (Exception e) {
                throw new RuntimeException("An exception occurred during proxy creation", e);
            }
        }
    }
}

3.1.2 Method#2: Using Mockito SpyBean

Before we go to the sample implementation of the title problem, I think it is worth to remember the basic information about Mockito and the used nomenclature.

  • Mockito – a library for mocking objects . Project site.
  • Mock – object used instead of the original implementation. Mock is only used to analyse interaction with it, i.e. how many times a given method has been called.
  • Stub – it is also an object replacing the original implementation, but used to return certain fixed values as responses to calls to its methods.
  • Spy – is a replacement for the original object, on which we can program a specific response as in the case of stub as well as execute the actual code of the method.

It is high time to present the main character of this article in action. I’m talking about Mockito, of course. So let’s see how much code you need to achieve the same effect as that with the use of a dynamic proxy.

package com.bettercoding.lab;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Spy;
import org.springframework.boot.test.context.SpringBootTest;

import java.math.BigDecimal;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doCallRealMethod;


@SpringBootTest
class BankAppMockitoTest {

    @Spy
    private BankApp.InternalBankService internalBankService;

    @InjectMocks
    private BankApp.BankService bankService;

    @Test
    void happyPathTest() {
        bankService.credit("account_1", BigDecimal.valueOf(100));
        bankService.credit("account_1", BigDecimal.valueOf(50));
        bankService.credit("account_2", BigDecimal.valueOf(200));
        bankService.credit("account_2", BigDecimal.valueOf(300));

        assertEquals(150, bankService.getAmount("account_1").longValue());
        assertEquals(500, bankService.getAmount("account_2").longValue());
    }

    @Test
    void negativePathTest() {

        Assertions.assertThrows(RuntimeException.class, () -> {
            doCallRealMethod()
                    .doCallRealMethod()
                    .doCallRealMethod()
                    .doThrow(RuntimeException.class)
                    .when(internalBankService).addToAccount(any(), any());

            bankService.transfer("account_1", "account_2", BigDecimal.valueOf(100));
            bankService.transfer("account_1", "account_2", BigDecimal.valueOf(50));
        });


        assertEquals(-100, bankService.getAmount("account_1").longValue());
        assertEquals(100, bankService.getAmount("account_2").longValue());
    }
}

As you probably noticed we only had to write the behaviour of Spy bean InternalBankService. To do this, we injected it with the @Spy annotation (lines 19-20). Then we programmed it in lines 40-44, so that in the case of calls to the addToAccount method it executed the original method code twice, and the third time it threw the RuntimeException. The last change we had to make was to change the @Autowired annotation to @InjectMocks. This annotation injects beans taking into account the mock defined with the Mockito framework.

4. Summary

It is known that there are many ways to solve the same problem in programming. And so, before I learned about the existence of Mockito, my solutions looks like those with the use of dynamic class proxy. Now, being aware of the existence and benefits of such a framework, would I choose the old solution again?

And what are your thoughts on this topic? Have you met something recently that changed your view on writing code or designing applications? Please leave a comment and maybe you will help me and other readers to get to know something new.

If the article was helpful to you, you can support me by:

Leave a Reply

avatar
  Subscribe  
Notify of
Close Menu