Unit Testing

অনেক কষ্ট করে একটি নতুন ফিচার ডেভেলপ করার পর লোকাল মেশিনে সেটি ঠিকঠাক চললেও প্রোডাকশনে গিয়ে ডাটাবেস ক্র্যাশ করা বা ভুল রেজাল্ট দেওয়া খুবই সাধারণ একটি ঘটনা। ডিবাগ করতে গিয়ে ঘণ্টার পর ঘণ্টা নষ্ট হওয়ার এই ভয়াবহতা থেকে বাঁচার সবচেয়ে কার্যকর উপায় হলো Unit Testing।

Unit Testing হলো এমন একটি প্রক্রিয়া, যেখানে তোমার লেখা প্রোগ্রামের প্রতিটি ক্ষুদ্রতম ইউনিট (যেমন— একটি ফাংশন, মেথড বা ক্লাস) আলাদাভাবে টেস্ট করা হয়, যাতে নিশ্চিত হওয়া যায় যে সেটি তার নির্দিষ্ট কাজটি নিখুঁতভাবে করছে। এটি শুধু একটি টেস্টিং টুল নয়, এটি একটি মাইন্ডসেট। এর মাধ্যমে তুমি এমন কোড লিখতে শেখো, যা নিজেই প্রমাণ করতে পারে যে সে ঠিকভাবে কাজ করছে।

একটি আদর্শ Unit Test এর বৈশিষ্ট্য

একটি ইউনিট টেস্ট সফল হতে হলে তার মধ্যে অবশ্যই নিচের গুণগুলো থাকতে হবে:

  • Fast (দ্রুতগতি): ইউনিট টেস্ট মিলিসেকেন্ডের মধ্যে রান করবে। হাজার হাজার টেস্ট রান করলেও যেন কয়েক সেকেন্ডের বেশি সময় না লাগে।

  • Isolated (বিচ্ছিন্ন): একটি টেস্টের ফলাফল কখনোই অন্য একটি টেস্টের ওপর নির্ভর করবে না। প্রতিটি টেস্ট স্বাধীনভাবে এক্সিকিউট হবে।

  • Repeatable (পুনরাবৃত্তিযোগ্য): তুমি টেস্টটি ১ বার রান করো বা ১০০০ বার—ফলাফল সবসময় একই আসতে হবে।

  • Deterministic (সুনির্দিষ্ট): বাইরের কোনো ফ্যাক্টর (যেমন নেটওয়ার্ক স্পিড বা সার্ভার ডাউন) ইউনিট টেস্টকে প্রভাবিত করতে পারবে না।

কোন কাজগুলো Unit Test এর আওতায় পড়ে না?

Unit Test শুধুমাত্র লজিক বা অ্যালগরিদম চেক করে। যদি তোমার টেস্ট নিচের কাজগুলোর কোনো একটি করে, তবে সেটি আর Unit Test থাকে না, সেটি Integration Test বা Feature Test এ পরিণত হয়:

  • ডাটাবেস থেকে ডেটা রিড (Read) বা রাইট (Write) করা।

  • এক্সটার্নাল API বা থার্ড-পার্টি সার্ভিসে নেটওয়ার্ক কল করা।

  • লোকাল ফাইল সিস্টেম থেকে ফাইল পড়া বা সেভ করা।

  • UI রেন্ডার করা বা ব্রাউজার ওপেন করে ইউজারের ইনপুট নেওয়া।

The AAA Pattern: টেস্ট লেখার স্ট্যান্ডার্ড আর্কিটেকচার

প্রফেশনাল ডেভেলপাররা ইউনিট টেস্ট লেখার সময় AAA (Arrange, Act, Assert) প্যাটার্ন ফলো করে থাকেন। এটি টেস্ট কোডকে পরিচ্ছন্ন ও পাঠযোগ্য রাখে:

  • Arrange (প্রস্তুতি): টেস্ট চালানোর জন্য প্রয়োজনীয় ডেটা, অবজেক্ট বা ভ্যারিয়েবলগুলো সেটআপ করা।

  • Act (অ্যাকশন): যে ফাংশন বা মেথডটি টেস্ট করতে চাও, তাকে কল করা এবং তার রেজাল্ট সেভ করা।

  • Assert (যাচাই): ফাংশন থেকে পাওয়া রেজাল্ট তোমার প্রত্যাশিত রেজাল্টের সাথে হুবহু মিলছে কি না, তা চেক করা।

Python Implementation: AAA Pattern এর উদাহরণ

পাইথনের বিল্ট-ইন unittest মডিউল ব্যবহার করে কীভাবে AAA প্যাটার্নে একটি ক্লিন ইউনিট টেস্ট লিখতে হয়, তা নিচে দেওয়া হলো:

import unittest

# The core logic / unit we want to test
def calculate_discount(price: float, discount_rate: float) -> float:
    if price < 0 or discount_rate < 0:
        raise ValueError("Price and discount cannot be negative")
    return price - (price * discount_rate)


class TestDiscountCalculator(unittest.TestCase):
    
    def test_calculate_discount_returns_correct_value(self):
        # 1. Arrange
        original_price = 100.0
        discount = 0.20

        # 2. Act
        final_price = calculate_discount(original_price, discount)

        # 3. Assert
        self.assertEqual(final_price, 80.0)

    def test_calculate_discount_with_negative_price_raises_error(self):
        # Arrange
        negative_price = -50.0
        discount = 0.10

        # Act & Assert
        with self.assertRaises(ValueError):
            calculate_discount(negative_price, discount)

if __name__ == "__main__":
    unittest.main()

Java Implementation: External Dependency ও Mocking

যেহেতু ইউনিট টেস্ট ডাটাবেস বা API এর সাথে কানেক্ট করতে পারে না, তাই এক্সটার্নাল ডিপেন্ডেন্সির ক্ষেত্রে আমরা Mock (নকল অবজেক্ট) ব্যবহার করি। নিচে Mockito ব্যবহার করে জাভাতে একটি উদাহরণ দেওয়া হলো:

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

// External dependency interface (e.g., calls a real bank API)
interface PaymentGateway {
    boolean charge(String userId, double amount);
}

// Our core unit
class OrderService {
    private final PaymentGateway paymentGateway;

    // Dependency injection makes mocking easier
    public OrderService(PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }

    public boolean processOrder(String userId, double amount) {
        if (amount <= 0.0) {
            return false;
        }
        return paymentGateway.charge(userId, amount);
    }
}

public class OrderServiceTest {
    
    @Test
    public void testProcessOrder_WithValidAmount_ReturnsTrue() {
        // 1. Arrange: Create a mock of the external dependency
        PaymentGateway mockGateway = mock(PaymentGateway.class);
        
        // Define what the mock should do when called
        when(mockGateway.charge("user_123", 50.0)).thenReturn(true);
        
        OrderService orderService = new OrderService(mockGateway);

        // 2. Act
        boolean result = orderService.processOrder("user_123", 50.0);

        // 3. Assert
        assertTrue(result);
        
        // Verify that the mock was actually called exactly once
        verify(mockGateway, times(1)).charge("user_123", 50.0);
    }
}

জনপ্রিয় Unit Testing Frameworks

প্রতিটি প্রোগ্রামিং ভাষার নিজস্ব শক্তিশালী টেস্টিং ইকোসিস্টেম রয়েছে:

ভাষা (Language)

জনপ্রিয় ফ্রেমওয়ার্ক (Frameworks)

Python

pytest (সবচেয়ে জনপ্রিয়), unittest

Java

JUnit 5, TestNG, Mockito (Mocking এর জন্য)

PHP

PHPUnit, Pest

JavaScript / TypeScript

Jest, Mocha, Vitest

Go

testing (Go এর বিল্ট-ইন প্যাকেজ)

C# / .NET

xUnit, NUnit

Unit Test লেখার Best Practices

তুমি যদি প্রফেশনাল লেভেলের ইউনিট টেস্ট লিখতে চাও, তবে নিচের নিয়মগুলো মেনে চলা উচিত:

  • টেস্টের নাম হতে হবে বর্ণনামূলক (Descriptive Naming): টেস্টের নাম পড়েই যেন বোঝা যায় এটি কী টেস্ট করছে। যেমন: test_add এর চেয়ে test_calculate_discount_returns_correct_value অনেক বেশি অর্থবহ।

  • এক টেস্টে একটিই ফোকাস: একটি টেস্ট মেথডের ভেতর অনেকগুলো লজিক বা মাল্টিপল Assert চেক করা উচিত নয়। একটি টেস্ট ফেইল করলে যেন তুমি সাথে সাথে বুঝতে পারো ঠিক কোন লজিকটি ভেঙেছে।

  • Setup এবং Teardown এর ব্যবহার: টেস্ট চালানোর আগে কোনো কমন অবজেক্ট বা ডেটা তৈরির প্রয়োজন হলে তা প্রতিটি টেস্টে বারবার না লিখে setUp() মেথডে রাখা উচিত।

  • নির্ভয়ে Refactor করার সাহস: টেস্ট কভারেজ ভালো থাকলে তুমি নির্ভয়ে প্রোডাকশন কোড মডিফাই বা অপ্টিমাইজ করতে পারবে। কারণ কোনো লজিক ব্রেক করলে টেস্ট রানার তোমাকে সাথে সাথেই সতর্ক করে দেবে।

  • Automation (CI/CD): টেস্টগুলোকে ম্যানুয়ালি রান করার পাশাপাশি গিটহাব অ্যাকশনস (GitHub Actions) বা জেনকিন্স (Jenkins) এর মতো CI/CD পাইপলাইনে যুক্ত করে দাও, যেন কোড পুশ করার সাথে সাথেই টেস্টগুলো স্বয়ংক্রিয়ভাবে রান হয়।