Introduction to unit testing in Java
Tags: java, unittest, junit5, mockito
Published: 2023-12-28 14:52:29

> The point of writing tests is not only to ensure that your code works now, but to ensure that the code will continue to work in the future.

### What does a typical test run involve?

1. Prepration
2. Provide test input
3. Running the test case
4. Provide expected output
5. Verify output (expected vs actual)
6. Do something if the test fails (expected and output do not match)

3, 5, 6 are common tasks while 1, 2, 4 change based on your test. Instead of doing everything yourself, offload the common tasks to a framework — JUnit is the defacto unit testing framework in Java. The current JUnit framework release is **5**.

### Problems with JUnit4

1. Outdated. > 10 years old
2. No support for newer testing patterns
3. Was playing catch up with newer java features
4. Had a monolothic architecture (one jar that had it all)

### JUnit5 Architecture

![JUnit5 Architecture](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t13x1xbw3te9mmd4dnyv.png)

### Setup

Let’s test the deposit and withdraw functionality of the Account aggregate:

    public class Transaction {

        public enum Type {
            CREDIT, DEBIT, HOLD
        };

        public Transaction(String accountId, Type type, BigDecimal amount, String reference) {
            this.id = UUID.randomUUID().toString();
            this.transactionTime = System.currentTimeMillis();
            this.accountId = accountId;
            this.type = type;
            this.amount = amount;
            this.reference = reference;
        }

        private final String id;

        private final String accountId;

        private final long transactionTime;

        private final Type type;

        private final BigDecimal amount;

        private final String reference;

        //Getters

    }

-----

    public class Account {

        private String id;

        private String userId;

        private BigDecimal balance = BigDecimal.ZERO;

        Transaction deposit(BigDecimal amount, String reference) {
            balance = balance.add(amount);
            return new Transaction(id, Transaction.Type.CREDIT, amount, reference);
        }

        Transaction withdraw(BigDecimal amount, String reference) {
            balance = balance.subtract(amount);
            return new Transaction(id, Transaction.Type.DEBIT, amount, reference);
        }

        //Getters and Setters

    }

Add the jupiter-engine and jupiter-api dependency.

```
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>junit-bom</artifactId>
            <version>5.8.2</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>  
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
```

Create a test class in the src/java/test directory using the same package convention (so that you get access to all default properties of the class you want to test) and annotate your methods with @Test (from the jupiter jar!) or use your IDE to create a test:

    class AccountTest {

        @Test
        public void testDeposit() {
            Account account = new Account();
            Transaction transaction1 = account.deposit(BigDecimal.TEN, "FY 23-24 qtr 2 interest");
            Transaction transaction2 = account.deposit(BigDecimal.ONE, "Transaction XYZ Surcharge waiver");

            assertEquals(BigDecimal.valueOf(11), account.getBalance());
            assertEquals(Transaction.Type.CREDIT, transaction1.getType());
            assertEquals(BigDecimal.TEN, transaction1.getAmount());

        }

    }

Running the test from the IDE:

![Assert Passed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijovrtmjerfdzyx51xeb.png)

#### Assertions (Leverage JUnit’s way of reporting an error!)

    import static org.junit.jupiter.api.Assertions.*; //static import - contains all the assert methods!

    assertEquals(expected, actual) //Example

Using assertions you can assert/verify if your code is producing the desired results or not. Your test will fail if these assertions fail. Example, let’s change our deposit method and introduce a bug:

    Transaction deposit(BigDecimal amount, String reference) {
        //We are not using the return value of the add method. 
        //Note: BigDecimal is an immutable object! 
        balance.add(amount);
        return new Transaction(id, Transaction.Type.CREDIT, amount, reference);
    }

If we run our test case now, it will fail:

![Assert failed](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9u1231f4rxozzxoqz1tp.png)

Be a little more descriptive when your test fails:

    assertEquals(BigDecimal.valueOf(11), account.getBalance(), "The account balance after two deposits of 10 and 1 should be 11");

![Assert descriptive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sofdfoygs7tgvgqj94tp.png)

_Asserting Exceptions using assertThrows_

    public class MathUtils {

        //Other methods omitted

        public int divide(int a, int b) {
            return a / b;
      }

    }

-----

    class MathUtilsTest {

        @Test
        void testDivideBy0() {
            MathUtils mathUtils = new MathUtils();
            /*
            assertThrows(Exception class, Executable)
            public interface Executable {
                void execute() throws Throwable;
            }
           */
           assertThrows(ArithmeticException.class, () -> mathUtils.divide(1, 0),
        "Divide by 0 should raise an arithmetic exception");
        }

    }

#### Running tests using maven instead of your IDE

If you try to run mvn test, the tests are not executed!

Use the maven surefire plugin. This will allow you to run tests using the mvn test command (The plugin binds a goal to execute all test when the test phase is executed). You can then potentially automate test execution in your CI/CD pipeline!

```
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <version>3.0.0-M7</version>
        </plugin>
    </plugins>
</build>
```

### The Test Life Cycle and Hooks (BeforeAll, BeforeEach, AfterEach, AfterAll)

![Test life cycle](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k4e6c7i8cx8e4s2ay3jn.png)

![Hooks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hv9q8pn6aaio2z7h47px.png)

```
class AccountTest

    private Account account;

    @BeforeAll
    public static void beforeAll() {
        System.out.println("In before all");
    }

    @BeforeEach
    public void beforeEach() {
        System.out.println("In before each");
        account = new Account();
    }

    @AfterEach
    public void afterEach() {
        System.out.println("In after each");
    }

    @AfterAll
    public static void afterAll() {
        System.out.println("In after all");
    }

    @Test
    public void testDeposit() {
        Transaction transaction1 = account.deposit(BigDecimal.TEN, "FY 23-24 qtr 2 interest");
        Transaction transaction2 = account.deposit(BigDecimal.ONE, "Transaction XYZ Surcharge waiver");

        assertEquals(BigDecimal.valueOf(11), account.getBalance(), "The account balance after two deposits of 10 and 1 should be 11");
        assertEquals(Transaction.Type.CREDIT, transaction1.getType());
        assertEquals(BigDecimal.TEN, transaction1.getAmount());
    }

    @Test
    public void testWithdraw() {
        Transaction transaction = account.withdraw(BigDecimal.TEN, "ATM withdrawal");

        assertEquals(BigDecimal.valueOf(-10), account.getBalance(), "The account balance after a withdrawal of 10 should be -10");
        assertEquals(Transaction.Type.DEBIT, transaction.getType());
        assertEquals(BigDecimal.TEN, transaction.getAmount());
    }

}
```

![Hooks result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nbtdidpv3k8xr94j7h77.png)

**Important** — The @BeforeAll and @AfterAll are called before the instance of the test class is created/after the instance of the test class is destroyed. Therefore, they should be static methods!

### Paramaterised Tests

Allows you to execute a single test method multiple times with different parameters

Also allows you to specify the source of your tests

In order to use parameterised test, add the **junit-jupiter-params** artifact

```
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <scope>test</scope>
</dependency>
```

Then annotate your test methods with **@ParameterizedTest** + Specify a Source. Example:

```
public class MathUtils {

  public double areaOfCircle(double radius) {
    return Math.PI * radius * radius;
  }

  public int add(int a, int b) {
    return a + b;
  }

  public int divide(int a, int b) {
    return a / b;
  }

}
```

```
@ParameterizedTest
@ValueSource(doubles = {1d, 2d, 3d, 4d, 5d})
void testCircleArea(double radius) {
  MathUtils math = new MathUtils();
  double expected = Math.PI * radius * radius;
  double actual = math.areaOfCircle(radius);

  assertEquals(expected, actual, "Area of circle is PI * r * r");
}
```

![Value source](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/py0md83ed50iscqz4sty.png)

As you can see, the single test method was run 5 times with values 1, 2, 3, 4, and 5 passed to the double radius parameter respectively.

With the **@ValueSource** you can pass a single literal value (ints, strings, floats, doubles, etc. to your test case)

Use **@NullSource**, **@EmptySource**, **@NullAndEmpty** source to pass null/empty sources

Use **@EnumSource** to pass enums

Use **@CsvSource** to pass in multiple literal arguments

```
@ParameterizedTest
@CsvSource({"1, 1, 2", "2, 2, 4", "5, 5, 10", "15, 15, 30", "50, 50, 100"})
void testAdd(int a, int b, int expected) {
  MathUtils math = new MathUtils();
  int actual = math.add(a, b);
  assertEquals(expected, actual, "adding " + a + ", " + b + " should result in " + expected);
}
```

![CSV source](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wukvha4k10po8yrdztak.png)

Use **@MethodSource** to provide more complex arguments

```
@ParameterizedTest
@MethodSource("divideSource")
void testDivideBy0(int a, int b) {
  /*
  assertThrows(Exception class, Executable)
  public interface Executable {
     void execute() throws Throwable;
  }
   */
  assertThrows(ArithmeticException.class, () -> this.math.divide(a, b),
      "Divide by 0 should raise an arithmetic exception");
}

//Should be static and return a stream/collection of Arguments
//Each Arguments item will initiate a test execution using said arguments!
//Can be in a separate class as well. You'll then need to use fully qualified name in the @MethodSource annotation
//Arguments.of(...Object) - Can pass complex arguments
static Stream<Arguments> divideSource() {
  return Stream.of(
      Arguments.of(1, 0),
      Arguments.of(2, 0),
      Arguments.of(3, 0)
  );
}
```

![Method source](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hp55pvqlpzwhu44fs6r4.png)

### Mockito

Mockito is a Java-based mocking framework that is used for unit testing of Java applications. It allows you to create mock objects that simulate the behavior of real objects, and verify their interactions and expectations. Mockito can be used with other testing frameworks, such as JUnit and is useful for testing components that depend on other components, but are not yet available or are difficult to access. By using mock objects, you can isolate the component under test and focus on its logic and functionality.

Add the **mockito-core** and the **mockito-junit-jupiter** (A 3rd party extension library for junit and mockito integration) dependency

```
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-core -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-core</artifactId>
  <version>4.9.0</version>
  <scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mockito/mockito-junit-jupiter -->
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>4.9.0</version>
  <scope>test</scope>
</dependency>
```

Let’s extend our Accounts functionality and add a service. This example assumes you have a SpringBoot project (with Spring Data). The Account and Transaction Repository are Spring Data repositories.

```
@Service
public class AccountApplicationService {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private TransactionRepository transactionRepository;

    @Transactional
    public void deposit(String accountId, BigDecimal amount, String reference) {
        Account account = accountRepository.getAccountById(accountId).orElseThrow();
        Transaction transaction = account.deposit(amount, reference);
        transactionRepository.save(transaction);
        accountRepository.save(account);
    }

}
```

Let’s try to test AccountApplicationService. To test it, I want to Mock the AccountRepository and TransactionRepository.

```
@ExtendWith(MockitoExtension.class)
class AccountApplicationServiceTest {

    private Account account;

    //Create a mock for this dependency
    @Mock
    private AccountRepository accountRepository;

    //Create a mock for this dependency
    @Mock
    private TransactionRepository transactionRepository;

    //@InjectMocks creates an instance of this class and injects the mocks that are created with the @Mock (or @Spy) annotations into this instance
    @InjectMocks
    private AccountApplicationService accountApplicationService;

    @BeforeEach
    public void beforeEach() {
        this.account = new Account();
        //Whenever the accountRepository.getAccountById method is called,
        //return the given account object
        Mockito.when(accountRepository.getAccountById(Mockito.anyString())).thenReturn(Optional.of(account));
    }

    @Test
    public void testDeposit() {
        accountApplicationService.deposit("temp", BigDecimal.TEN, "FY 23-24 qtr 2 interest");
        assertEquals(BigDecimal.TEN, account.getBalance());
        //Verify if the deposit call resulted in a call to the save method of the transactionRepository
        Mockito.verify(transactionRepository).save(Mockito.any());
    }

}
```