CodeGym /Courses /Module 5. Spring /Lecture 263: Hands-on: writing unit tests for services

Lecture 263: Hands-on: writing unit tests for services

Module 5. Spring
Level 21 , Lesson 2
Available

In this lecture we'll focus on the practical side: writing unit tests for services. We'll create tests for key business-logic components of one of the microservices, using tools you're already familiar with.


Getting ready to write Unit tests

Unit tests are the foundation of testing during development. They verify small pieces of code isolated from external dependencies. For example, you want to test a service method that calculates a discount without involving the database, another service's API, or third-party libraries.

Goals:

  1. Isolate the code under test.
  2. Verify the correctness of business logic.
  3. Make sure edge cases and exceptional situations are handled correctly.

Test environment

First, make sure your project is set up for testing. Check that you have the following dependencies:

pom.xml for Maven:


<dependencies>
    <!-- JUnit 5 -->
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Mockito -->
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-core</artifactId>
        <scope>test</scope>
    </dependency>

    <!-- Spring Boot Test -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.mockito</groupId>
                <artifactId>mockito-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>

build.gradle for Gradle:


dependencies {
    // JUnit 5
    testImplementation 'org.junit.jupiter:junit-jupiter'

    // Mockito
    testImplementation 'org.mockito:mockito-core'

    // Spring Boot Test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Example application: Discount shop

Let's take a microservice for processing discounts as an example. We have a DiscountService that calculates the discount amount based on the user's category and the number of purchased items.

Here's the service itself:


import org.springframework.stereotype.Service;

@Service
public class DiscountService {

    public double calculateDiscount(String userCategory, int itemCount) {
        if (itemCount <= 0) {
            throw new IllegalArgumentException("Item count must be greater than 0");
        }

        switch (userCategory.toLowerCase()) {
            case "vip":
                return itemCount * 0.2; // 20% discount
            case "regular":
                return itemCount > 5 ? itemCount * 0.1 : 0.0; // 10% discount if itemCount > 5
            default:
                return 0.0; // No discount
        }
    }
}

Step 1: Set up the test class

By convention, create a test class named DiscountServiceTest. Make sure it's located under src/test/java.


package com.example.service;

import org.junit.jupiter.api.Test;

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

/**
 * Tests for DiscountService.
 */
public class DiscountServiceTest {

    private final DiscountService discountService = new DiscountService();

    @Test
    void testCalculateDiscountForVipUser() {
        double discount = discountService.calculateDiscount("VIP", 10);
        assertEquals(2.0, discount, "VIP user should get 20% discount");
    }

    @Test
    void testCalculateDiscountForRegularUser() {
        double discount = discountService.calculateDiscount("regular", 6);
        assertEquals(0.6, discount, "Regular user should get 10% discount for more than 5 items");
    }

    @Test
    void testCalculateDiscountForUnknownUser() {
        double discount = discountService.calculateDiscount("guest", 5);
        assertEquals(0.0, discount, "Guest users should not get a discount");
    }

    @Test
    void testCalculateDiscountWithZeroItems() {
        assertThrows(IllegalArgumentException.class, () ->
            discountService.calculateDiscount("VIP", 0),
            "Should throw exception when item count is zero or less"
        );
    }
}

Discussion of the tests

  1. Positive scenarios: tests verify correct results for "VIP" and "regular" users.
  2. Negative scenarios: a test for the "guest" category and a test with zero item count.
  3. Exceptions: checking that invalid parameters throw IllegalArgumentException.

Step 2: Adding interaction with Mock (Mockito)

Now assume DiscountService depends on another service, e.g., UserRepository, which returns user info. We can use Mockito to create a mock.

Inject the dependency:


@Service
public class DiscountService {

    private final UserRepository userRepository;

    public DiscountService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public double calculateDiscount(String userId, int itemCount) {
        if (itemCount <= 0) {
            throw new IllegalArgumentException("Item count must be greater than 0");
        }

        String userCategory = userRepository.getUserCategory(userId);
        switch (userCategory.toLowerCase()) {
            case "vip":
                return itemCount * 0.2;
            case "regular":
                return itemCount > 5 ? itemCount * 0.1 : 0.0;
            default:
                return 0.0;
        }
    }
}

Testing with Mockito:


import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

public class DiscountServiceTest {

    private UserRepository userRepository;
    private DiscountService discountService;

    @BeforeEach
    void setUp() {
        userRepository = mock(UserRepository.class);
        discountService = new DiscountService(userRepository);
    }

    @Test
    void testCalculateDiscountWithMockedUser() {
        when(userRepository.getUserCategory("123")).thenReturn("VIP");

        double discount = discountService.calculateDiscount("123", 10);

        assertEquals(2.0, discount);
        verify(userRepository, times(1)).getUserCategory("123");
    }

    @Test
    void testCalculateDiscountForUnknownUser() {
        when(userRepository.getUserCategory("456")).thenReturn("guest");

        double discount = discountService.calculateDiscount("456", 5);

        assertEquals(0.0, discount);
        verify(userRepository, times(1)).getUserCategory(anyString());
    }
}

  1. Creating a mock: we create a mock for UserRepository — a fake implementation not tied to a real database.
  2. Stubbing methods: use when().thenReturn() to specify the return value.
  3. Verifying calls: verify() checks that the method was called on the mock with the expected parameters.

Common mistakes and how to fix them

  1. Incorrect mocking: if you forget to specify when().thenReturn(), the mock will return null by default.
  2. Not using verify: verifying calls helps ensure the method under test interacts with the mock correctly.
  3. Uncaught exceptions: if you don't check exceptions, tests might miss critical issues.

Practical use

The ability to write unit tests is valuable for any backend development. Interviewers often ask about mocking dependencies and writing tests for complex business logic. In real projects this speeds up development because bugs are caught early.

Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION