JUnit Test Cases ලියමු: Java Software Quality වැඩි කරගන්න | SC Guide

JUnit Test Cases ලියමු: Java Software Quality වැඩි කරගන්න | SC Guide

කොහොමද යාලුවනේ! ඔන්න අද අපි කතා කරන්න යන්නේ Software Engineering ක්ෂේත්‍රයේ හරිම වැදගත් කොටසක් ගැන – ඒ තමයි Test Cases ලියන එක. විශේෂයෙන්ම, අපි අද බලමු Java applications වල Unit Testing කරන්න පාවිච්චි කරන ජනප්‍රියම Framework එකක් වන JUnit පාවිච්චි කරලා කොහොමද හරියට Test Cases ලියන්නේ කියලා.

අපි හැමෝම දන්නවනේ, Code එකක් ලියද්දි Bugs නැති කරන්න කොච්චර try කලත්, සමහර වෙලාවට පොඩි වැරදි වගයක් රිංගලා Production එකටම යනවා. එතකොට ඉතින් Customers ලාටත් කරදරයි, අපිටත් ඔළුවේ කැක්කුමයි, ආයෙත් Bug Fix කරන්න කාලයත් නාස්ති වෙනවා. මේකට හොඳම විසඳුමක් තමයි Test Cases හරියට ලියන එක. හරියට Test Cases ලියනවා කියන්නේ අපේ Code එක විශ්වාසදායකයි කියලා සහතික කරගන්න හොඳම ක්‍රමය. ඒ වගේම, අලුතින් Code එකට Features add කරනකොට හෝ Bug Fix කරනකොට කලින් වැඩ කරපු Features කැඩෙනවද කියලා බලන්නත් මේ Test Cases අපිට ලොකු උදව්වක් වෙනවා. ඉතින්, අපි අද ගැඹුරින් බලමු JUnit Framework එකත් එක්ක Test Cases ලියන මූලික සංකල්ප ටිකක් ගැන.

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

සරලවම කිව්වොත්, JUnit කියන්නේ Java applications වල Unit Testing කරන්න පාවිච්චි කරන Open-source Framework එකක්. Unit Testing කියන්නේ මොකක්ද? ඒකෙන් කරන්නේ Application එකේ තියෙන පොඩිම පොඩි කොටස් (මේවා Units කියලා හඳුන්වනවා) තනි තනිව test කරන එක. උදාහරණයක් විදිහට, ඔයා ලියපු Method එකක් හරි, Class එකක තියෙන පොඩි Functionality එකක් හරි හරියට වැඩ කරනවද කියලා මේකෙන් Test කරන්න පුළුවන්. මේකෙන් අපිට පුළුවන් Code එකේ පොඩිම තැන්වල තියෙන වැරදි ඉක්මනින්ම හොයාගෙන ඒවා හදාගන්න.

JUnit Framework එකේ දැනට තියෙන අලුත්ම Version එක තමයි JUnit 5. මේක කලින් Version වලට වඩා ගොඩක් දියුණු කරලා තියෙනවා වගේම, Test Cases ලියන එක ගොඩක් පහසු කරන්න අලුත් Features ගණනාවක්ම මේකේ තියෙනවා. අපි අද මේ Article එකේදී වැඩිපුරම අවධානය යොමු කරන්නේ JUnit 5 වලට.

JUnit Framework එක ඔයාගේ Project එකට add කරගන්න නම්, ඔයා Maven හෝ Gradle වගේ Build Tool එකක් පාවිච්චි කරනවා නම්, pom.xml (Maven) හෝ build.gradle (Gradle) file එකට Dependency එක add කරන්න ඕනේ. පහතින් තියෙන්නේ සාමාන්‍යයෙන් JUnit 5 වලට අවශ්‍ය වන Dependencies.


<dependencies>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.10.0</version> <!-- Use the latest version -->
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.10.0</version> <!-- Use the latest version -->
        <scope>test</scope>
    </dependency>
</dependencies>

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' // Use the latest version
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.10.0' // Use the latest version
}

පළමු Test එක ලියමු (Writing Your First Test)

හරි, දැන් අපි බලමු කොහොමද සරලම Test Case එකක් ලියන්නේ කියලා. මුලින්ම, අපි Test කරන්න අවශ්‍ය Class එකක් හදාගමු. අපි ගණන් හදන පොඩි Calculator Class එකක් හදමු.

// src/main/java/com/example/Calculator.java
package com.example;

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

    public int subtract(int a, int b) {
        return a - b;
    }

    public double divide(double a, double b) {
        if (b == 0) {
            throw new IllegalArgumentException("Cannot divide by zero");
        }
        return a / b;
    }
}

දැන් අපි මේ Calculator Class එකට අදාළ Test Class එක හදමු. සාමාන්‍යයෙන් Test Class එක තියෙන්නේ src/test/java කියන Folder එකේ (IDE එකක Project එකක් හදනකොට මේ Folder Structure එක Auto generate වෙනවා). Test Class එකේ නම test කරන Class එකේ නමට Test කියලා එකතු කරලා හදනවා. උදාහරණයක් විදිහට, Calculator Class එකට CalculatorTest වගේ. මේක Standard Practice එකක්.

// src/test/java/com/example/CalculatorTest.java
package com.example;

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

class CalculatorTest {

    @Test
    void testAdd() {
        // 1. Arrange
        Calculator calculator = new Calculator();
        int expected = 5;

        // 2. Act
        int actual = calculator.add(2, 3);

        // 3. Assert
        assertEquals(expected, actual, "2 + 3 should be 5");
    }
}

මේ Code එකේ තියෙන වැදගත් කොටස් ටිකක් බලමු:

  • import org.junit.jupiter.api.Test;: මේකෙන් අපි @Test Annotation එක Import කරගන්නවා.
  • import static org.junit.jupiter.api.Assertions.*;: මේකෙන් අපි JUnit Framework එකේ තියෙන Assertions Methods ටික Direct පාවිච්චි කරන්න පුළුවන් වෙන විදිහට Import කරගන්නවා.
  • @Test: මේ Annotation එක තමයි ප්‍රධානම දේ. මේකෙන් JUnit Framework එකට කියනවා මේ Method එක (testAdd()) Test Method එකක් කියලා. JUnit මේක Execute කරන්නේ මේ Annotation එක දැක්කට පස්සේ.
  • void testAdd(): Test Method එකක Return Type එක void වෙන්න ඕනේ. Method එකේ නමෙන් Test කරන Functionality එක පැහැදිලි වෙන විදිහට දාන එක හොඳ Practice එකක්.
  • assertEquals(expected, actual, "2 + 3 should be 5");: මේක තමයි Assertion එක. මේකෙන් කරන්නේ අපේ Method එකෙන් එන Actual Result එක (calculator.add(2, 3)) අපි බලාපොරොත්තු වෙන Expected Result එකට (5) සමානද කියලා බලන එක. තුන්වෙනි Parameter එක optional Message එකක්. Test එක Fail වුණොත් මේ Message එක පෙන්නනවා, ඒක Test එක Fail වෙන්න හේතුව හොයාගන්න උදව් වෙනවා.

Assertions ගැන තවදුරටත් (More on Assertions)

Assertions තමයි Test Cases වල හදවත. මේවා තමයි අපේ Code එක හරියට වැඩ කරනවද කියලා බලන්න පාවිච්චි කරන Statements. JUnit වල තියෙන Assertion Methods ගණනාවක්ම තියෙනවා. අපි ඒවයින් බහුලව පාවිච්චි වෙන ටිකක් බලමු:

  • assertEquals(expected, actual, [message]): දෙකක් සමානද බලනවා. (අපි කලින් දැක්කා)

assertArrayEquals(expectedArray, actualArray, [message]): Arrays දෙකක් සමානද බලනවා. (ඒවායේ අනුපිළිවෙල සහ Elements සමාන විය යුතුයි)

@Test
void testArrayEquality() {
    int[] expected = {1, 2, 3};
    int[] actual = {1, 2, 3};
    assertArrayEquals(expected, actual, "Arrays should be equal");
}

assertThrows(expectedType, executable, [message]): දී ඇති executable එකෙන් බලාපොරොත්තු වන Exception එක throw වෙනවද කියලා බලනවා. මේක වැදගත්, මොකද Code එකේ වැරදි Input වලට Exceptions handle කරනවද කියලා Test කරන්න පුළුවන්.

@Test
void testDivideByZero() {
    Calculator calculator = new Calculator();
    // Check if dividing by zero throws an IllegalArgumentException
    assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0),
            "Dividing by zero should throw IllegalArgumentException");
}

assertNotNull(object, [message]): Object එක null නොවේද බලනවා.

@Test
void testNonNullObject() {
    Object obj = new Object();
    assertNotNull(obj, "Object should not be null");
}

assertNull(object, [message]): Object එක null ද බලනවා.

@Test
void testNullObject() {
    Object obj = null;
    assertNull(obj, "Object should be null");
}

assertFalse(condition, [message]): Condition එක false ද බලනවා.

@Test
void testIsNegative() {
    int num = -10;
    assertFalse(num > 0, "Number should be negative");
}

assertTrue(condition, [message]): Condition එක true ද බලනවා.

@Test
void testIsPositive() {
    int num = 5;
    assertTrue(num > 0, "Number should be positive");
}

Test Fixtures හදාගමු (Setting Up Test Fixtures)

ගොඩක් වෙලාවට, අපේ Test Methods කිහිපයකටම එකම Setup එකක් අවශ්‍ය වෙනවා. උදාහරණයක් විදිහට, අපේ Calculator Class එකේ Test Methods හැම එකටම Calculator Object එකක් අවශ්‍ය වෙනවා. හැම Test Method එකකම new Calculator() කියලා ලියන එක අපතේ යන වැඩක් නේද? මේ වගේ පොදු Setup එකක් හදාගන්න අපි Test Fixtures පාවිච්චි කරනවා.

JUnit 5 වලදී Test Fixtures හදන්න අපි @BeforeEach සහ @AfterEach වගේ Annotations පාවිච්චි කරනවා.

  • @BeforeEach: මේ Annotation එක තියෙන Method එක, @Test Annotation එක තියෙන සෑම Test Method එකක්ම Run වෙන්න කලින් Run වෙනවා. මේක ගොඩක්ම පාවිච්චි කරන්නේ Test එකකට කලින් අවශ්‍ය Objects Initialize කරන්න, Database Connection එකක් හදාගන්න වගේ දේවල් වලට.
  • @AfterEach: මේ Annotation එක තියෙන Method එක, @Test Annotation එක තියෙන සෑම Test Method එකක්ම Run වුණාට පස්සේ Run වෙනවා. මේක පාවිච්චි කරන්නේ Test එක ඉවර වුණාට පස්සේ Resources Clean up කරන්න, Database Connection වහන්න වගේ දේවල් වලට.

අපි අපේ CalculatorTest Class එකට මේක එකතු කරලා බලමු.

// src/test/java/com/example/CalculatorTest.java
package com.example;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class CalculatorTest {

    private Calculator calculator; // Declare an instance variable

    @BeforeEach
    void setUp() {
        // This method runs before each @Test method
        calculator = new Calculator();
        System.out.println("setUp() called before a test."); // For demonstration
    }

    @AfterEach
    void tearDown() {
        // This method runs after each @Test method
        calculator = null; // Clean up resources if necessary
        System.out.println("tearDown() called after a test."); // For demonstration
    }

    @Test
    void testAdd() {
        // Calculator object is already initialized by @BeforeEach
        int expected = 5;
        int actual = calculator.add(2, 3);
        assertEquals(expected, actual, "2 + 3 should be 5");
    }

    @Test
    void testSubtract() {
        int expected = 2;
        int actual = calculator.subtract(5, 3);
        assertEquals(expected, actual, "5 - 3 should be 2");
    }

    @Test
    void testDivide() {
        double expected = 2.5;
        double actual = calculator.divide(5, 2);
        // Use a delta for double comparisons due to floating-point precision issues
        assertEquals(expected, actual, 0.001, "5 / 2 should be 2.5"); 
    }

    @Test
    void testDivideByZero() {
        assertThrows(IllegalArgumentException.class, () -> calculator.divide(10, 0),
                "Dividing by zero should throw IllegalArgumentException");
    }
}

මේ උදාහරණයේදී, calculator Object එක හැම Test එකක්ම Run වෙන්න කලින් setUp() Method එකෙන් Initialize වෙනවා. ඒ වගේම, හැම Test එකක්ම ඉවර වුණාට පස්සේ tearDown() Method එකෙන් calculator Object එක null කරනවා. මේකෙන් වෙන්නේ හැම Test එකක්ම Clean State එකකින් පටන් ගන්න එක. මේක Independent Tests ලියන්න හරිම වැදගත්.

සටහන: @BeforeAll සහ @AfterAll කියන Annotations දෙකත් තියෙනවා. ඒවා Class එකේ හැම Test එකක්ම Run වෙන්න කලින් (@BeforeAll) හෝ හැම Test එකක්ම Run වුණාට පස්සේ (@AfterAll) එක පාරක් පමණක් Run වෙනවා. මේවා ගොඩක් වෙලාවට පාවිච්චි කරන්නේ Database Connection Pool එකක් වගේ Heavy Resources Setup කරන්න.

හොඳ Test Cases ලියන්න Tips (Tips for Writing Good Test Cases)

Test Cases ලියන එක විතරක් මදි, හොඳ Test Cases ලියන්නත් ඕනේ. හොඳ Test Cases වලින් අපේ Code Quality එක වැඩි වෙනවා වගේම, Bug අඩු කරගන්නත් ලොකු උදව්වක් වෙනවා. මෙන්න හොඳ Test Cases ලියන්න පුළුවන් Tips ටිකක්:

  1. Arrange-Act-Assert (AAA) Pattern එක පාවිච්චි කරන්න: මේක Test Cases ලියද්දි පාවිච්චි කරන ජනප්‍රියම Pattern එකක්. මේකෙන් Test එකේ Readability එක වැඩි වෙනවා.අපේ testAdd() Method එකේ මේ Pattern එක යොදාගෙන තියෙනවා ඔයාලට පේනවා ඇති.
    • Arrange (සකස් කිරීම): Test එකට අවශ්‍ය Data, Objects, Mock Dependencies වගේ දේවල් Initialise කරන්න.
    • Act (ක්‍රියාත්මක කිරීම): Test කරන Method එක හෝ Functionality එක Call කරන්න.
    • Assert (තහවුරු කිරීම): Result එක අපි බලාපොරොත්තු වන විදිහටම ආවද කියලා Assertions පාවිච්චි කරලා Verify කරන්න.
  2. Independent Tests ලියන්න: හැම Test Method එකක්ම අනිත් Test Methods වලින් ස්වාධීන වෙන්න ඕනේ. එක Test එකක් Fail වුණොත් අනිත් Test වලට බලපෑමක් වෙන්න දෙන්න එපා. @BeforeEach වගේ Fixtures මේකට උදව් වෙනවා.
  3. Meaningful Test Names දෙන්න: Test Method එකේ නමෙන් ඒ Test එකෙන් කරන්නේ මොකක්ද සහ මොන Outcome එකක්ද බලාපොරොත්තු වෙන්නේ කියලා පැහැදිලි වෙන්න ඕනේ. උදාහරණයක් විදිහට: testAdd_PositiveNumbers_ReturnsCorrectSum(), testDivide_ByZero_ThrowsException() වගේ.
  4. Edge Cases Test කරන්න: සාමාන්‍ය Scenarios විතරක් Test කරලා මදි. Input වලට එන්න පුළුවන් Limit Values (උදා: Minimum/Maximum values), Invalid Inputs (උදා: Negative numbers, Zero, Null), Empty Collections වගේ Edge Cases හැම තිස්සෙම Test කරන්න.
  5. One Assertion Per Test (or few related ones): පුළුවන් නම් හැම Test එකකටම එක Assertion එකක් හෝ ඉතා සමීප Assertions කිහිපයක් විතරක් යොදාගන්න. මේකෙන් Test එක Fail වුණොත් මොකක්ද වැරැද්ද කියලා ඉක්මනින්ම හොයාගන්න පුළුවන්.
  6. Fast Tests ලියන්න: Unit Tests ගොඩක් ඉක්මනින් Run වෙන්න ඕනේ. Slow Tests නිසා Developers ලා Tests Run කරන එක මඟහරින්න පුළුවන්.

අවසන් වශයෙන්

ඉතින් යාලුවනේ, අද අපි JUnit Framework එක පාවිච්චි කරලා Test Cases ලියන මූලික කරුණු ගැන කතා කළා. @Test Annotation එක පාවිච්චි කරලා Test Methods ලියන හැටි, Assertions පාවිච්චි කරලා Results Verify කරන හැටි, ඒ වගේම Test Fixtures (@BeforeEach, @AfterEach) පාවිච්චි කරලා Test Environment එක Setup කරන හැටි වගේම, හොඳ Test Cases ලියන්න පුළුවන් Tips ටිකකුත් අපි ඉගෙන ගත්තා.

Test Cases ලියන එක මුලදී පොඩ්ඩක් අමාරු වගේ පෙනුනත්, මේක ඔයාලගේ Code එකේ Quality එක වැඩි කරන්නත්, Bugs අඩු කරන්නත් තියෙන හොඳම ක්‍රමයක්. මතක තියාගන්න, හොඳ Software එකක් කියන්නේ හොඳට Test කරපු Software එකක්. මේක Practice කරන්න ඕනේ Skill එකක්. අද අපි කතා කරපු දේවල් ඔයාලගේ Project වලට අදාළව Try කරලා බලන්න. Code එකේ Quality එක නංවා ගන්න මේක ගොඩක් වැදගත් වේවි!

මේ Article එක ගැන ඔයාලගේ අදහස්, ප්‍රශ්න පහලින් comment කරන්න අමතක කරන්න එපා. අපි ඊළඟ Article එකෙන් හම්බවෙමු!