Mockito වලින් Unit Test කරන හැටි - SC Guide | Software Engineering

Mockito වලින් Unit Test කරන හැටි - SC Guide | Software Engineering

ආයුබෝවන්, සොෆ්ට්වෙයාර් ඉංජිනේරු ක්ෂේත්‍රයේ ඉන්න අපේ හැමෝටම!

අද අපි කතා කරන්න යන්නේ, Unit Testing කියන දේ ගැන වගේම, ඒක සාර්ථකව කරන්න අපිට නැතුවම බැරි උපකරණයක් වන Mockito ගැන. විශේෂයෙන්ම අපි බලමු කොහොමද අපේ Services වල තියෙන dependencies Mock කරලා, හොඳටම Isolation කරලා Unit Tests ලියන්නේ කියලා. මේක බොහොම practical guide එකක් විදියට ඔයාලට ගොඩක් වැදගත් වෙයි.

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

අපි කට්ටියම දන්නවා, සොෆ්ට්වෙයාර් හදනකොට, අපි ලියන code එකේ හැම කොටසක්ම හරියට වැඩ කරනවද කියලා බලන එක කොච්චර වැදගත්ද කියලා. ඒක තමයි Unit Testing කියන්නේ. හැබැයි අපි ලියන code එකේ එක කොටසක් (ඒ කියන්නේ "Unit" එකක්) තව කොටසක් එක්ක සම්බන්ධ වෙලා වැඩ කරනකොට, ඒක හරියට test කරන එක ටිකක් අමාරු වෙන්න පුළුවන්.

උදාහරණයක් විදියට හිතන්නකෝ, ඔයා User කෙනෙක්ව database එකට save කරන service එකක් හදනවා කියලා. මේ service එකට database එකත් එක්ක කතා කරන්න Repository එකක් ඕන වෙනවා. දැන් ඔයා මේ service එක test කරනකොට, ඔයාට ඇත්තටම database එකක් අවශ්‍ය නැහැ. අවශ්‍ය වෙන්නේ service එක හරියට Repository එකත් එක්ක කතා කරනවද කියලා බලන එක විතරයි. මෙන්න මේ වගේ අවස්ථාවලට තමයි Mocking කියන එක එන්නේ.

Mocking කියන්නේ, අපේ Unit එකක් depend වෙන real object එකක් වෙනුවට, ඒක simulate කරන, ඒ වගේම behave වෙන fake object එකක් හදන එකට. මේ fake object එකට අපි කියනවා "Mock object" කියලා. මේ Mock object එක හරහා අපිට පුළුවන් අපේ Unit එකේ ක්‍රියාකාරීත්වය, ඒකේ dependencies නිසා ඇතිවෙන side effects නැතුවම test කරන්න. හිතන්න, ඔයා වාහනයක් හදනවා. එන්ජිම හදන්න කලින්, වාහනයේ අනිත් කොටස් හරිද කියලා බලන්න, ඔයාට තාවකාලික එන්ජිමක් වගේ දෙයක් දාන්න පුළුවන්නේ. ඒ වගේ තමයි Mocking කියන්නේ.

ඇයි Mockito?

Mocking Libraries ගොඩක් තියෙනවා. PowerMock, EasyMock වගේ ඒවා. හැබැයි Java ලෝකයේ Mockito කියන්නේ ගොඩක් ජනප්‍රිය, වගේම පහසුවෙන් පාවිච්චි කරන්න පුළුවන් Library එකක්. Mockito වල ප්‍රධාන වාසි කිහිපයක් තියෙනවා:

  • කියවන්න පහසුයි (Readable): Mockito code එක බැලුවම, ඒකෙන් මොකක්ද වෙන්නේ කියලා තේරුම් ගන්න ගොඩක් පහසුයි. Test cases කියවනකොට ඒක ලොකු වාසියක්.
  • ලියන්න පහසුයි (Easy to Write): annotations පාවිච්චි කරලා ඉක්මනටම Mock objects හදාගන්න පුළුවන්.
  • Flexible: විවිධ scenarios වලට ගැලපෙන විදියට Mock objects හදාගන්න පුළුවන්.

මේ නිසා තමයි ගොඩක් කට්ටිය Unit Testing වලට Mockito තෝරා ගන්නේ.

Mockito Basics - පටන් ගමු!

හරි, දැන් අපි බලමු කොහොමද Mockito පටන් ගන්නේ කියලා. මුලින්ම ඔයාලගේ project එකට Mockito dependency එක එකතු කරගන්න ඕනේ.

Maven

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.x.x</version> <!-- නවතම version එක බලලා දාන්න -->
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.x.x</version> <!-- නවතම version එක බලලා දාන්න -->
    <scope>test</scope>
</dependency>

Gradle

dependencies {
    testImplementation 'org.mockito:mockito-core:5.x.x' // නවතම version එක බලලා දාන්න
    testImplementation 'org.mockito:mockito-junit-jupiter:5.x.x' // නවතම version එක බලලා දාන්න
}

ඊළඟට, Mockito වල තියෙන ප්‍රධාන annotations දෙකක් ගැන දැනගමු:

  • @Mock: මේකෙන් අපිට Mock object එකක් හදන්න පුළුවන්.
  • @InjectMocks: මේකෙන් අපිට test කරන්න ඕන actual instance එකක් හදන්න පුළුවන්. Mockito මේකේ dependencies ස්වයංක්‍රීයව inject කරනවා.

Test class එකක් පටන් ගන්නකොට @ExtendWith(MockitoExtension.class) කියන annotation එක දාන්න අමතක කරන්න එපා. ඒකෙන් තමයි Mockito annotations වැඩ කරන්නේ.

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

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

@ExtendWith(MockitoExtension.class)
class MyServiceTest {

    @Mock
    private MyDependency myDependency; // මේක තමයි Mock කරන්නේ

    @InjectMocks
    private MyService myService; // මේක තමයි Test කරන්නේ

    @BeforeEach
    void setUp() {
        // MockitoAnnotations.openMocks(this); // @ExtendWith නිසා මේක ඕන වෙන්නේ නෑ
    }

    @Test
    void testServiceMethod() {
        // 1. Mock කරන object එක හැසිරිය යුතු ආකාරය define කරනවා
        Mockito.when(myDependency.someMethod()).thenReturn("Mocked Value");

        // 2. Test කරන්න ඕන method එක execute කරනවා
        String result = myService.performAction();

        // 3. Result එක verify කරනවා
        assertEquals("Mocked Value", result);

        // 4. Mock කරන object එකේ method එක call උනාද කියලා verify කරනවා
        Mockito.verify(myDependency).someMethod();
    }
}

මේ උදාහරණයෙන් ඔයාලට තේරෙනවා ඇති, Mockito.when().thenReturn() පාවිච්චි කරලා, Mock කරපු object එකේ method එකක් call උනාම, මොන වගේ value එකක් return කරන්න ඕනෙද කියලා අපිට කියන්න පුළුවන්. ඒ වගේම, Mockito.verify() පාවිච්චි කරලා, අපේ actual service එකේ method එක execute වෙනකොට, Mock කරපු dependency එකේ අදාල method එක call උනාද කියලා බලන්නත් පුළුවන්.

සර්විස් එකක dependencies Mock කරන හැටි (Practical Example)

දැන් අපි බලමු real-world scenario එකක්. අපි හිතමු අපිට User කෙනෙක්ව ගේන UserService එකක් තියෙනවා කියලා. මේ UserService එකට UserRepository එකක් අවශ්‍ය වෙනවා User details database එකෙන් retrieve කරගන්න.

අපේ Services සහ Repositories

මුලින්ම, අපේ Java classes ටික බලමු:

// User.java
public class User {
    private Long id;
    private String name;
    private String email;

    // Constructors, getters, setters, etc.
    public User(Long id, String name, String email) {
        this.id = id;
        this.name = name;
        this.email = email;
    }

    public Long getId() { return id; }
    public String getName() { return name; }
    public String getEmail() { return email; }
}

// UserRepository.java (Interface)
import java.util.Optional;

public interface UserRepository {
    Optional<User> findById(Long id);
    // Other CRUD operations
}

// UserService.java
import java.util.Optional;

public class UserService {
    private final UserRepository userRepository;

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

    public Optional<User> getUserById(Long id) {
        return userRepository.findById(id);
    }

    public User createUser(User user) {
        // Imagine complex logic here, e.g., validation, password hashing
        return user; // Simplified for example
    }
}

UserService එක Test කරන හැටි

දැන් අපි බලමු, UserService එකේ getUserById method එක test කරන්නේ කොහොමද කියලා. අපිට UserRepository එකේ database call එක ඇත්තටම කරන්න ඕනේ නැහැ. ඒ වෙනුවට අපි ඒක Mock කරනවා.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository; // මේක තමයි Mock කරන්නේ

    @InjectMocks
    private UserService userService; // මේක තමයි Test කරන්නේ

    @Test
    void getUserById_UserExists() {
        // Arrange: Test එකට අවශ්‍ය දත්ත සකස් කරගන්නවා
        Long userId = 1L;
        User mockUser = new User(userId, "Nimal", "[email protected]");

        // Mock behaviour: userRepository.findById(1L) call කලොත් mockUser return කරන්න
        when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));

        // Act: Test කරන්න ඕන method එක call කරනවා
        Optional<User> result = userService.getUserById(userId);

        // Assert: ප්‍රතිඵලය අපේක්ෂිතද කියලා පරීක්ෂා කරනවා
        assertTrue(result.isPresent()); // User කෙනෙක් ඉන්නවද කියලා බලනවා
        assertEquals(userId, result.get().getId());
        assertEquals("Nimal", result.get().getName());
        assertEquals("[email protected]", result.get().getEmail());

        // Verify: userRepository.findById(userId) method එක එක පාරක් call උනාද කියලා බලනවා
        verify(userRepository).findById(userId);
    }

    @Test
    void getUserById_UserDoesNotExist() {
        // Arrange: Test එකට අවශ්‍ය දත්ත සකස් කරගන්නවා
        Long userId = 2L;

        // Mock behaviour: userRepository.findById(2L) call කලොත් Optional.empty() return කරන්න
        when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // Act: Test කරන්න ඕන method එක call කරනවා
        Optional<User> result = userService.getUserById(userId);

        // Assert: ප්‍රතිඵලය අපේක්ෂිතද කියලා පරීක්ෂා කරනවා
        assertTrue(result.isEmpty()); // User කෙනෙක් නැද්ද කියලා බලනවා

        // Verify: userRepository.findById(userId) method එක එක පාරක් call උනාද කියලා බලනවා
        verify(userRepository).findById(userId);
    }

    @Test
    void createUser_ValidUser() {
        // Arrange: Valid User object එකක් හදනවා
        User newUser = new User(null, "Kamal", "[email protected]");

        // createUser method එකට UserRepository call එකක් නැති නිසා, Mocking අවශ්‍ය නැහැ
        // හැබැයි future වලදී save operation එකක් ආවොත් Mock කරන්න පුළුවන්.
        // For demonstration, let's say we expect the same user back.
        // If userRepository.save(any(User.class)) was called, we could mock that.

        // Act: Test කරන්න ඕන method එක call කරනවා
        User createdUser = userService.createUser(newUser);

        // Assert: ප්‍රතිඵලය අපේක්ෂිතද කියලා පරීක්ෂා කරනවා
        assertEquals("Kamal", createdUser.getName());
        assertEquals("[email protected]", createdUser.getEmail());

        // Verify: මේ method එකේදී userRepository එකේ කිසිම method එකක් call වෙන්නේ නෑ කියලා verify කරන්න පුළුවන්
        verify(userRepository, Mockito.never()).findById(Mockito.anyLong());
        // Or if there was a save method: verify(userRepository).save(newUser);
    }
}

මේ උදාහරණයේදී ඔයාලට පේනවා ඇති අපි UserService එක test කරනවා, නමුත් UserRepository එකේ database calls ඇත්තටම සිදුවෙන්නේ නැහැ. ඒ වෙනුවට when().thenReturn() හරහා අපි fake data return කරනවා. මේකෙන් අපේ test එක වේගවත් වෙනවා වගේම, database එකේ අවුල් වගේ external factors නිසා test එක fail වෙන එකත් නවතිනවා. ඒ වගේම, verify() පාවිච්චි කරලා, අපේ UserService එක UserRepository එක හරියට පාවිච්චි කරනවද කියලා බලන්නත් පුළුවන්.

Practical Tips & Tricks (ප්‍රායෝගික උපදෙස් සහ උපක්‍රම)

Mockito වල තව ගොඩක් දේවල් තියෙනවා. මෙන්න ටිකක් ප්‍රයෝජනවත් වෙන tips:

  • @Spy: Mock object එකක් වගේම, real object එකක method call කරලා, ඒකේ behaviour ටිකක් වෙනස් කරන්න අවශ්‍ය නම් @Spy පාවිච්චි කරන්න පුළුවන්. මේක ටිකක් advanced concepts එකක්, ඒ නිසා පටන් ගන්න අයට Mocking වලින්ම ඉස්සෙල්ලා වැඩේ පටන් ගන්න කියලා මම suggest කරනවා.
  • Clear Mocks: @BeforeEach හෝ @AfterEach වලදී Mockito.reset(yourMock) පාවිච්චි කරලා test එකක් ඉවර උනාට පස්සේ Mock එක reset කරන්න පුළුවන්. හැබැයි @ExtendWith(MockitoExtension.class) පාවිච්චි කරනකොට මේක ස්වයංක්‍රීයව වෙන නිසා ගොඩක් වෙලාවට අවශ්‍ය වෙන්නේ නැහැ.

Argument Captors: Mock කරපු method එකකට call කරනකොට මොන වගේ arguments යවනවද කියලා බලන්න ArgumentCaptor පාවිච්චි කරන්න පුළුවන්.

ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
userService.createUser(newUser); // Imagine createUser saves to repo
verify(userRepository).save(userCaptor.capture());
User capturedUser = userCaptor.getValue();
assertEquals("Kamal", capturedUser.getName());

Void Methods Mock කිරීම: සමහර වෙලාවට අපිට return value එකක් නැති (void) method එකක් Mock කරන්න වෙනවා. ඒකට doNothing().when() හෝ doThrow().when() පාවිච්චි කරන්න පුළුවන්.

doNothing().when(userRepository).deleteById(Mockito.anyLong());
doThrow(new RuntimeException("Database error")).when(userRepository).save(Mockito.any(User.class));

Argument Matchers: Mockito.any(), Mockito.anyString(), Mockito.eq() වගේ ඒවා පාවිච්චි කරලා ඕනෑම argument එකකට හෝ නිශ්චිත argument එකකට Mock behaviour define කරන්න පුළුවන්.

when(userRepository.findById(Mockito.anyLong())).thenReturn(Optional.of(mockUser));
when(userRepository.findByName(Mockito.eq("Nimal"))).thenReturn(Optional.of(mockUser));

සෑම විටම මතක තබා ගන්න, Unit Test එකකින් test කරන්න ඕනේ එකම එක Unit එකක් විතරයි. Dependencies Mock කරන්නේ ඒ Unit එකේ ක්‍රියාකාරීත්වය හරියට test කරන්න පුළුවන් වෙන්නයි.

අවසන් වශයෙන්...

ඉතින්, අද අපි Mockito පාවිච්චි කරලා Java application එකක Services වල dependencies Mock කරලා Unit Tests ලියන්නේ කොහොමද කියලා විස්තරාත්මකව කතා කළා. Mocking කියන්නේ Unit Testing වලදී නැතුවම බැරි දෙයක්. විශේෂයෙන්ම Microservices වගේ architecture වල වැඩ කරනකොට මේක ගොඩක් ප්‍රයෝජනවත්.

මේ Concepts ටික ඔයාලට ප්‍රයෝජනවත් වෙන්න ඇති කියලා හිතනවා. මතක තියාගන්න, සොෆ්ට්වෙයාර් ඉංජිනේරුවරයෙක් විදියට test cases ලියන එක කියන්නේ ඔයාගේ code එකේ Quality එක වැඩි කරගන්න හොඳම ක්‍රමයක්. ඒ නිසා, මේ Mockito concepts practical project වලට apply කරලා බලන්න. එතකොට තමයි හොඳටම අල්ලගන්න පුළුවන්.

ඔයාලට මේ ගැන ප්‍රශ්න තියෙනවා නම්, නැත්නම් වෙන මොනවා හරි ගැන දැනගන්න ඕන නම් පහළින් comment එකක් දාන්න. අපි ඒ ගැන කතා කරමු! තව අලුත් article එකකින් හමුවෙමු, හැමෝටම ජය!