Mockito වලින් Java Unit Test ලේසි කරගමු! | Java Unit Testing SC Guide

Mockito වලින් Java Unit Test ලේසි කරගමු! | Java Unit Testing SC Guide

Java Unit Testing with Mockito: The SC Guide

කොහොමද යාලුවනේ! Software Engineering කියන්නේ දැන් ලංකාවෙත් ගොඩක් දෙනෙක් අතර ජනප්‍රිය වෙන ක්ෂේත්‍රයක්නේ. ඒ වගේම හොඳ Quality එකක Software එකක් හදන්න නම් Test කරන එක අනිවාර්යයි. මේකෙන් තමයි අපේ code එක හරියටම වැඩ කරනවාද, අපි බලාපොරොත්තු වෙන විදියට හැසිරෙනවාද කියලා sure කරගන්න පුළුවන් වෙන්නේ. ඒ අතරින් Unit Testing කියන්නේ මේකෙදි අපිට අතහිත දෙන ප්‍රධානම ටෙක්නික් එකක්.

හැබැයි මේ Unit Tests ලියද්දි අපිට මුණගැහෙන ප්‍රශ්නයක් තමයි Dependencies. අපේ class එකක් test කරද්දි ඒක වෙන class එකක් මත, නැත්තම් Database එකක්, Network service එකක්, File System එකක් වගේ external resource එකක් මත රඳා පවතිනවා වෙන්න පුළුවන්. අන්න ඒ වගේ වෙලාවට Test එක හරියට ලියන එක ටිකක් අමාරු වෙනවා, නැත්තම් Test එක Slow වෙනවා. ඒකට විසඳුමක් තමයි Mocking කියන්නේ. විශේෂයෙන්ම Java වලට Mockito කියන Library එක.

අද අපි කතා කරමු Mockito පාවිච්චි කරලා කොහොමද අපේ Unit Tests Smart විදියට ලියන්නේ කියලා. අපි මේක පියවරෙන් පියවරට කතා කරමු, මොකද ඔයාලට මේ concepts පැහැදිලිව තේරෙන එක තමයි වැදගත්!

මොකක්ද මේ Mocking කියන්නේ?

Mocking කියන්නේ මොකක්ද කියලා තේරුම් ගන්න අපි පොඩි උදාහරණයක් බලමු. හිතන්න, ඔයා අලුත් car engine එකක් හදලා තියෙනවා කියලා. දැන් මේ engine එක test කරන්න ඕනේ. Engine එක test කරන්න whole car එකම හයි කරන්න ඕනෙද? නැහැ නේද? අපිට පුළුවන් Engine එකට ඕන කරන fuel, power supply වගේ දේවල් simulate කරලා, Engine එක විතරක් වෙනම test කරන්න. මේක තමයි Mocking වල core idea එක.

Software වලදිත් මේක එහෙමයි. අපි Service Layer එකේ තියෙන code එකක් test කරනවා කියලා හිතමු. මේ Service එක data ගන්න, save කරන්න Database එකකට access කරන Repository එකක් මත රඳා පවතිනවා වෙන්න පුළුවන්. දැන් අපිට test කරන්න ඕනේ Service එකේ business logic එක විතරයි. Database එක එක්ක ඇත්තටම කතා කරන්න ගියොත්,

  • Test එක Slow වෙනවා.
  • Database එකේ actual data වෙනස් වෙන්න පුළුවන්.
  • Database එක down නම් test එක fail වෙනවා.

මේ වගේ ප්‍රශ්න එනවා. මේකට විසඳුම තමයි අපි Repository එකේ “dummy” හෝ “fake” version එකක් හදාගන්න එක. මේ dummy එකට අපි කියලා දෙනවා “මෙන්න මේ method එක call කරොත්, මෙන්න මේ වගේ result එකක් දෙන්න” කියලා. මේ dummy object එකට තමයි Mock object එකක් කියන්නේ.

Mocking වල ප්‍රධාන වාසි ටිකක් තමයි:

  • Isolated Tests: අපි test කරන class එක විතරක් isolate කරලා, ඒකේ dependencies ගැන කරදර නොවී test කරන්න පුළුවන්.
  • Faster Execution: External services එක්ක connect වෙන්නේ නැති නිසා Tests ගොඩක් වේගවත් වෙනවා.
  • Test Error Scenarios: ඇත්තටම database error එකක් එනකල් ඉන්නේ නැතුව, mock එකට error එකක් throw කරන්න කියලා අපිටම set කරන්න පුළුවන්.

Mockito කියන්නේ මොකක්ද?

Mockito කියන්නේ Java වලට තියෙන ඉතාමත් ජනප්‍රිය Open-Source Mocking Framework එකක්. මේකේ simplicity එකයි, readable syntax එකයි නිසා, unit tests ලියන එක හරිම පහසු වෙනවා. Mockito JUnit වගේ test frameworks එක්ක හොඳට integrate වෙනවා. Mock objects හදන්න, ඒවායේ behaviour එක define කරන්න, ඒ වගේම mock objects එක්ක interaction (ඒවායේ methods call වුනාද වගේ දේවල්) verify කරන්න Mockito වලින් පුළුවන්.

Mockito පාවිච්චි කරලා Test කරමු!

හරි, දැන් අපි කෙලින්ම code වලට බහිමු! මුලින්ම අපිට Mockito dependency එක අපේ Project එකට එකතු කරගන්න ඕනේ. අපි Maven පාවිච්චි කරනවා නම්, pom.xml එකට මේ dependency එක එකතු කරගන්න:

<dependencies>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId> <!-- For JUnit 5 -->
        <version>5.8.0</version> <!-- Latest version may vary -->
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.10.0</version> <!-- Latest version may vary -->
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.10.0</version> <!-- Latest version may vary -->
        <scope>test</scope>
    </dependency>
</dependencies>

mockito-junit-jupiter artifact එක JUnit 5 එක්ක වැඩ කරන්න. ඔයා JUnit 4 පාවිච්චි කරනවා නම් mockito-core එක use කරන්න පුළුවන්.

උදාහරණයක්: User Management Service එකක්

අපි හිතමු අපිට User Management Service එකක් තියෙනවා කියලා. මේ Service එක User data Database එකකින් ගන්න UserRepository එකක් පාවිච්චි කරනවා.

1. Models සහ Interfaces

මුලින්ම, අපේ simple User POJO එකයි, UserRepository Interface එකයි හදාගමු:

// src/main/java/com/scguide/model/User.java
package com.scguide.model;

import java.util.Objects;

public class User {
    private Long id;
    private String name;
    private String email;

    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    // Getters and Setters
    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
               Objects.equals(name, user.name) &&
               Objects.equals(email, user.email);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, email);
    }

    @Override
    public String toString() {
        return "User{" +
               "id=" + id +
               ", name='" + name + '\'' +
               ", email='" + email + '\'' +
               '}';
    }
}
// src/main/java/com/scguide/repository/UserRepository.java
package com.scguide.repository;

import com.scguide.model.User;

public interface UserRepository {
    User findById(Long id);
    boolean save(User user);
    boolean delete(Long id);
}

2. Service Class (Class under test)

// src/main/java/com/scguide/service/UserService.java
package com.scguide.service;

import com.scguide.model.User;
import com.scguide.repository.UserRepository;

public class UserService {
    private UserRepository userRepository;

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

    public User getUserById(Long id) {
        if (id <= 0) {
            throw new IllegalArgumentException("User ID must be positive");
        }
        return userRepository.findById(id);
    }

    public boolean createUser(User user) {
        if (user == null || user.getName() == null || user.getName().isEmpty()) {
            throw new IllegalArgumentException("User name cannot be empty");
        }
        // Assume some complex business logic here
        System.out.println("Processing user for creation: " + user.getName());
        return userRepository.save(user);
    }

    public boolean deleteUser(Long id) {
        if (id <= 0) {
            throw new IllegalArgumentException("User ID must be positive");
        }
        // Before deleting, perhaps check if user exists or other logic
        User existingUser = userRepository.findById(id);
        if (existingUser == null) {
            return false; // User not found
        }
        return userRepository.delete(id);
    }
}

3. Unit Test Class (Using Mockito)

අපි Test Class එක හදමු. Mockito එක්ක වැඩ කරද්දි @Mock සහ @InjectMocks annotations පාවිච්චි කරන එක හරිම පහසුයි. @ExtendWith(MockitoExtension.class) එකෙන් කියන්නේ මේ Test Class එක Mockito එක්ක වැඩ කරන්න පුළුවන් කියලා.

// src/test/java/com/scguide/service/UserServiceTest.java
package com.scguide.service;

import com.scguide.model.User;
import com.scguide.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.ArgumentCaptor;
import org.mockito.junit.jupiter.MockitoExtension;

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

@ExtendWith(MockitoExtension.class)
public class UserServiceTest {

    @Mock // Mockito will create a mock instance of UserRepository
    private UserRepository userRepositoryMock;

    @InjectMocks // Mockito will inject the above mock into this UserService instance
    private UserService userService;

    @Test
    void testGetUserByIdSuccess() {
        // 1. Arrange (Setup the mock behavior)
        User dummyUser = new User(1L, "Kamal Perera", "[email protected]");
        // When findById(1L) is called on userRepositoryMock, return dummyUser
        when(userRepositoryMock.findById(1L)).thenReturn(dummyUser);

        // 2. Act (Call the method under test)
        User result = userService.getUserById(1L);

        // 3. Assert (Verify the result and interactions)
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("Kamal Perera", result.getName());
        assertEquals("[email protected]", result.getEmail());

        // Verify that findById(1L) was called exactly once on the mock
        verify(userRepositoryMock, times(1)).findById(1L);
        // Verify no other interactions with the mock
        verifyNoMoreInteractions(userRepositoryMock);
    }

    @Test
    void testGetUserByIdNotFound() {
        // Arrange
        when(userRepositoryMock.findById(2L)).thenReturn(null);

        // Act
        User result = userService.getUserById(2L);

        // Assert
        assertNull(result);
        verify(userRepositoryMock, times(1)).findById(2L);
    }

    @Test
    void testGetUserByIdInvalidInput() {
        // Assert that calling getUserById with 0L throws IllegalArgumentException
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            userService.getUserById(0L);
        });
        assertEquals("User ID must be positive", exception.getMessage());
        // Verify no interaction with userRepositoryMock for invalid input
        verifyNoInteractions(userRepositoryMock);
    }

    @Test
    void testCreateUserSuccess() {
        // Arrange
        User newUser = new User(null, "Nimal Bandara", "[email protected]");
        // Use any() matcher because the actual User object might not be reference-equal
        when(userRepositoryMock.save(any(User.class))).thenReturn(true);

        // Act
        boolean result = userService.createUser(newUser);

        // Assert
        assertTrue(result);
        // Verify that save method was called exactly once with any User object
        verify(userRepositoryMock, times(1)).save(any(User.class));
    }

    @Test
    void testCreateUserInvalidInput() {
        // Test case for empty name
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            userService.createUser(new User(null, "", "[email protected]"));
        });
        assertEquals("User name cannot be empty", exception.getMessage());
        verifyNoInteractions(userRepositoryMock);
    }

    @Test
    void testDeleteUserSuccess() {
        // Arrange
        User existingUser = new User(3L, "Supun Kumar", "[email protected]");
        when(userRepositoryMock.findById(3L)).thenReturn(existingUser);
        when(userRepositoryMock.delete(3L)).thenReturn(true);

        // Act
        boolean result = userService.deleteUser(3L);

        // Assert
        assertTrue(result);
        verify(userRepositoryMock, times(1)).findById(3L);
        verify(userRepositoryMock, times(1)).delete(3L);
    }

    @Test
    void testDeleteUserNotFound() {
        // Arrange
        when(userRepositoryMock.findById(4L)).thenReturn(null);

        // Act
        boolean result = userService.deleteUser(4L);

        // Assert
        assertFalse(result);
        verify(userRepositoryMock, times(1)).findById(4L);
        verify(userRepositoryMock, never()).delete(anyLong()); // Ensure delete was NOT called
    }

    @Test
    void testDeleteUserInvalidInput() {
        // Assert
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
            userService.deleteUser(-1L);
        });
        assertEquals("User ID must be positive", exception.getMessage());
        verifyNoInteractions(userRepositoryMock);
    }

    @Test
    void testCreateUserAndCaptureArgument() {
        // Arrange
        User userToCreate = new User(null, "Chamal Silva", "[email protected]");
        when(userRepositoryMock.save(any(User.class))).thenReturn(true);

        // Act
        userService.createUser(userToCreate);

        // Assert - Capture the argument passed to save method
        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        verify(userRepositoryMock).save(userCaptor.capture());

        User capturedUser = userCaptor.getValue();
        assertEquals("Chamal Silva", capturedUser.getName());
        assertEquals("[email protected]", capturedUser.getEmail());
        assertNull(capturedUser.getId()); // ID should be null before saving
    }
}

මේ code එක දිහා හොඳට බලන්න. අපි කරන්නේ userRepositoryMock එකේ behavior එක define කරන එක (when().thenReturn()). ඊට පස්සේ userService එකේ method එක call කරලා, අවසානයේ verify() කරලා බලනවා userRepositoryMock එකේ methods හරියටම call වුනාද කියලා.

වැදගත් Mockito Methods:

  • mock(Class<T> classToMock): Mock object එකක් හදන්න.
  • when(mock.method()).thenReturn(value): Mock method එකක් call කරද්දි return කරන්න ඕන දේ define කරන්න.
  • when(mock.method()).thenThrow(exception): Mock method එකක් call කරද්දි exception එකක් throw කරන්න.
  • doNothing().when(mock).voidMethod(): void return කරන method එකක් mock කරන්න.
  • verify(mock, times(n)).method(): Mock method එකක් n වතාවක් call වුනාද කියලා බලන්න.
  • verify(mock, never()).method(): Mock method එකක් කවදාවත් call වුනේ නැද්ද කියලා බලන්න.
  • verify(mock, atLeastOnce()).method(): Mock method එකක් අඩුම තරමේ එක වතාවක් වත් call වුනාද කියලා බලන්න.
  • verifyNoMoreInteractions(mock): දීලා තියෙන mock එකේ verify කරපු calls ඇරෙන්න, වෙන calls නැද්ද කියලා බලන්න.
  • verifyNoInteractions(mock): දීලා තියෙන mock එකේ කිසිම interaction එකක් වුනේ නැද්ද කියලා බලන්න.

තවත් ටිප්ස් සහ ට්‍රික්ස්

Argument Matchers (any(), eq())

සමහර වෙලාවට අපිට හරියටම object instance එකක් specify කරන්න බැරි වෙන්න පුළුවන් (උදා: new User(...) වගේ අලුත් object එකක් method එකකට pass කරද්දි). ඒ වගේ වෙලාවට Mockito වල Argument Matchers පාවිච්චි කරන්න පුළුවන්:

// ඕනෑම String එකකට match කරන්න
when(userRepositoryMock.findByName(any(String.class))).thenReturn(dummyUser);

// ඕනෑම Long අගයකට match කරන්න
when(userRepositoryMock.findById(anyLong())).thenReturn(dummyUser);

// එක Argument එකක් exact value එකක් වෙලා, අනිත් එක any() වෙන්න ඕන නම්
when(userService.doSomething(eq("specific"), any(String.class))).thenReturn(true);

සැලකිය යුතුයි: ඔයා any() වගේ Argument Matcher එකක් පාවිච්චි කරනවා නම්, ඒ method එකේ තියෙන අනිත් arguments වලටත් Argument Matchers ම පාවිච්චි කරන්න ඕනේ. නැත්තම් Mockito error එකක් දෙනවා.

Argument Captor

සමහර වෙලාවට අපිට ඕනේ mock method එකකට pass කරපු argument එක මොකක්ද කියලා check කරන්න. ඒ කියන්නේ, method එක call කරද්දි මොන object එකද pass වුනේ කියලා බලන්න. මේකට Argument Captor පාවිච්චි කරන්න පුළුවන්:

@Test
void testSaveUserAndVerifyPassedArgument() {
    User userToSave = new User(null, "Kasun Perera", "[email protected]");
    when(userRepositoryMock.save(any(User.class))).thenReturn(true);

    userService.createUser(userToSave);

    // ArgumentCaptor එකක් හදාගන්නවා User class එකට
    ArgumentCaptor<User> userArgumentCaptor = ArgumentCaptor.forClass(User.class);

    // verify කරද්දි, userArgumentCaptor එකට argument එක capture කරන්න කියනවා
    verify(userRepositoryMock).save(userArgumentCaptor.capture());

    // Capture කරගත්තු argument එක ගන්නවා
    User capturedUser = userArgumentCaptor.getValue();

    // දැන් ඒ captured object එකේ values check කරන්න පුළුවන්
    assertEquals("Kasun Perera", capturedUser.getName());
    assertEquals("[email protected]", capturedUser.getEmail());
    assertNull(capturedUser.getId());
}

Best Practices

  • Mock Only Dependencies: ඔබ test කරන class එක Mock කරන්න එපා. ඒකේ Dependencies විතරක් Mock කරන්න.
  • Test One Thing at a Time: හැම test method එකක්ම එක specific scenario එකක් test කරන්න ලියන්න.
  • Avoid Over-Mocking: ගොඩක් mocks හදන එකෙන්, test code එක fragile වෙන්න පුළුවන් (implementation details වෙනස් වුනොත් tests කැඩෙනවා). ඇත්තටම අවශ්‍ය Dependencies විතරක් Mock කරන්න.
  • Use verify(): Mocked methods call වුනාද, කොයිතරම් වාර ගාණක් call වුනාද, මොන arguments එක්කද call වුනේ වගේ දේවල් check කරන්න verify() පාවිච්චි කරන්න අමතක කරන්න එපා.

නිගමනය

ඉතින් යාලුවනේ, Mockito කියන්නේ Java Unit Testing වලදි අපේ වැඩ ගොඩක් ලේසි කරන Powerful Tool එකක්. මේකෙන් අපිට පුළුවන් External Dependencies ගැන කරදර නොවී, අපේ Code එකේ Logic එක හරියටම වැඩ කරනවද කියලා Sure කරගන්න. ඒ වගේම test runs ගොඩක් වේගවත් කරගන්නත් පුළුවන්.

අද අපි කතා කරපු Concepts ටික ඔයාලාට හොඳට තේරෙන්න ඇති කියලා හිතනවා. Mocking වල මූලික අදහස, Mockito Setup කරන විදිය, basic mock behaviour define කරන විදිය, සහ advanced features කිහිපයක් (Argument Matchers, Argument Captor) ගැන අපි කතා කළා.

දැන් ඔයාලත් මේක තනියම Try කරලා බලන්න. පොඩි Project එකක් හදලා මේ Mocking Concepts ටික Apply කරලා බලන්න. එතකොට තමයි හරියටම මේක ප්‍රැක්ටිස් වෙන්නේ. Test Driven Development (TDD) වලට යොමු වෙනවා නම්, Mockito වගේ framework එකක් නැතුව බැරිම වෙයි!

මොනාහරි ප්‍රශ්න තියෙනවා නම්, පහලින් Comment කරන්න. මම පුළුවන් විදියට උත්තර දෙන්නම්. ආයෙත් හමුවෙමු තවත් වටිනා Software Engineering Article එකකින්!

ජයවේවා!