Python Pytest Unit Testing Sinhala Tutorial | Fixtures & Parametrized Tests SC Guide

Python Pytest Unit Testing Sinhala Tutorial | Fixtures & Parametrized Tests SC Guide

සාමාන්‍යයෙන් Software එකක් හදනකොට, අපි හැමෝම දන්නවා ඒක හරියට වැඩ කරනවද කියලා බලන එක ගොඩක් වැදගත්. ඒකට තමයි Unit Testing කියන්නේ. අපේ කෝඩ් එකේ පොඩිම කොටසක් (Unit එකක්) හරි විදියට වැඩ කරනවද කියලා බලන එක මේකෙන් සිද්ධ වෙනවා.

Python වලට Unit Testing කරන්න ගියාම, ගොඩක් දෙනෙක් දන්න ප්‍රධානම Module එක තමයි unittest. ඒක හොඳයි, ඒත් සමහර වෙලාවට Test ලියන එක ටිකක් සංකීර්ණ වෙන්න පුළුවන්, අනවශ්‍ය විදියට කෝඩ් වැඩියෙන් ලියන්නත් වෙනවා.

අන්න ඒකට තියෙන නියම විසඳුමක් තමයි Pytest. Pytest කියන්නේ Python වලට තියෙන පට්ටම Testing Framework එකක්. මේකෙන් Test ලියන එක ගොඩක් සරලයි, ලස්සනයි, ඒ වගේම ගොඩක් දේවල් කරන්නත් පුළුවන්. අද අපි Pytest ගැන මුල ඉඳලා ඉගෙන ගමු. ඇයි මේක මේ තරම් ජනප්‍රිය වුණේ, ඒකේ තියෙන Fixtures, Parameterized Testing වගේ සුපිරි Features කොහොමද පාවිච්චි කරන්නේ, තව Best Practices වගේ දේවල් කතා කරමු.

අද කාලේ Python Developersලා අතර Pytest මේ තරම් ජනප්‍රිය වෙන්න හේතු ගොඩක් තියෙනවා. අපි එකින් එක බලමු.

1. සරල Assertions (Simple Assertions)

Pytest වල ලොකුම වාසියක් තමයි Tests ලියන්න unittest වගේ Class එකක් inherit කරන්න ඕනේ නැති එක. ඒ වගේම assertEqual(), assertTrue() වගේ Method වෙනුවට, Pytest වලට සාමාන්‍ය Python වල assert Statement එක පාවිච්චි කරන්න පුළුවන්. මේකෙන් Test කෝඩ් එක කියවන්නත් ලියන්නත් ගොඩක් ලේසියි.

උදාහරණයක් විදියට:

# සාමාන්‍ය unittest Module එකෙන්
import unittest

def add(a, b):
    return a + b

class TestAddFunction(unittest.TestCase):
    def test_add_positive_numbers(self):
        self.assertEqual(add(2, 3), 5)

# Pytest වලින්
def test_add_positive_numbers():
    assert add(2, 3) == 5

දැක්කනේ කොච්චර සරලද කියලා? Pytest Automaticallyම assert Statement එක Fail වුණොත් මොකද වුණේ කියලා පැහැදිලිව පෙන්නනවා.

2. ස්වයංක්‍රීය Test සොයාගැනීම (Automatic Test Discovery)

Pytest තියෙන තැනින් Test File හොයාගන්න ගොඩක් දක්ෂයි. ඔයාට වෙනම Test Suite එකක් හදන්න ඕනේ නැහැ. Pytest ස්වයංක්‍රීයවම මේ දේවල් කරනවා:

  • test_*.py නැත්නම් *_test.py වගේ නම් කරලා තියෙන Files හොයනවා.
  • ඒ Files ඇතුළේ තියෙන test_* කියලා පටන් ගන්න Functions (හෝ Methods) Test විදියට හඳුනා ගන්නවා.

ඒක නිසා අපිට Test ලියන එක විතරයි කරන්න තියෙන්නේ, Framework එකට Test මොනවද කියලා කියන්න ඕනේ නැහැ. නියමයි නේද?

3. Fixtures (Test Setup/Teardown)

Test කරන්න කලින් මොකක් හරි setup එකක් කරන්න ඕනෙද? (උදා: Database එකකට සම්බන්ධ වෙන්න, Files හදන්න). නැත්නම් Test කරලා ඉවර වෙලා මොකක් හරි cleanup එකක් කරන්න ඕනෙද? (උදා: Database connection එක වහන්න, Temporary Files delete කරන්න). මේ හැමදේම Fixtures වලින් ලේසියෙන්ම කරගන්න පුළුවන්. Fixtures ගැන අපි ඊළඟට විස්තරාත්මකව කතා කරමු.

4. Parameterized Testing

එකම Test එකක්, වෙනස් වෙනස් Inputs එක්ක ගොඩක් වාරයක් Run කරන්න ඕනෙද? Pytest වල තියෙන @pytest.mark.parametrize කියන Feature එකෙන් මේක ලේසියෙන්ම කරගන්න පුළුවන්. මේකෙනුත් Test කෝඩ් එක ගොඩක් අඩු වෙනවා, ඒ වගේම කියවන්නත් ලේසියි.

5. පුළුල් කළ හැකි බව (Extensibility)

Pytest එකට ගොඩක් Plugins තියෙනවා. Code Coverage බලන්න, Test Reports හදන්න, Database Testing කරන්න වගේ දේවල් වලට මේ Plugins පාවිච්චි කරන්න පුළුවන්. ඒකෙන් Pytest වල හැකියාව තව තවත් වැඩි වෙනවා.

Pytest Fixtures: ඔබේ Test එකේ 'සෙටප්' එක ලේසි කරමු (Pytest Fixtures: Let's Make Your Test Setup Easy)

මතකද අපි කිව්වා Test කරන්න කලින් යම් Setup එකක් කරන්න ඕන කියලා? ඒ වගේම Test එක ඉවර වෙලා cleanup කරන්නත් ඕනේ. Fixtures කියන්නේ මේ Setup/Teardown operations ටික කරලා, ඒ වගේම Test එකට අවශ්‍ය දත්ත (Data) නැත්නම් Object එකක් සපයන Functions.

සරලවම කිව්වොත්, Fixture එකක් කියන්නේ Test එකක් Run කරන්න කලින් අවශ්‍ය පරිසරය හදලා දෙන, නැත්නම් Test එකට අවශ්‍ය Object එකක් ලබා දෙන Function එකක්.

Fixture එකක් හදන්නේ කොහොමද?

Fixture එකක් හදන්න @pytest.fixture Decorator එක පාවිච්චි කරනවා. මේක සාමාන්‍ය Function එකක් විදියටම හදන්න පුළුවන්. මේ Function එකෙන් Return කරන ඕනෑම දෙයක්, Test Function එකක Argument එකක් විදියට පාවිච්චි කරන්න පුළුවන්.

හරි, අපි පොඩි උදාහරණයක් බලමු. අපි හිතමු අපිට Calculator එකක් තියෙනවා කියලා. ඒකේ add කියන Function එක Test කරන්න ඕනේ. හැම Test එකකටම එකම numbers ටිකක් පාවිච්චි කරනවා නම්, අපිට ඒකට Fixture එකක් හදන්න පුළුවන්.

calculator.py File එක:

# calculator.py
def add(a, b):
    return a + b

def subtract(a, b):
    return a - b

test_calculator.py File එක (Fixture එකක් සමඟ):

# test_calculator.py
import pytest
from calculator import add

@pytest.fixture
def default_numbers():
    print("\nSetting up default numbers...") # Test එක run වෙනකොට මේක print වෙයි
    x = 10
    y = 5
    yield x, y # Fixture එකෙන් values return කරනවා
    print("Cleaning up default numbers...") # Test එක ඉවර වුණාම මේක print වෙයි

def test_add_with_fixture(default_numbers):
    x, y = default_numbers
    result = add(x, y)
    assert result == 15

def test_subtract_with_fixture(default_numbers):
    x, y = default_numbers
    result = add(x, -y) # subtract function එක නැති නිසා මෙතනත් add use කරනවා
    assert result == 5

මේ උදාහරණයේදී, default_numbers කියන්නේ Fixture එකක්. test_add_with_fixture සහ test_subtract_with_fixture කියන Test Functions වලට default_numbers කියන Fixture එක Argument එකක් විදියට දීලා තියෙනවා. Pytest Automaticallyම මේ Fixture එක Run කරලා ඒකෙන් Return කරන Values Test Functions වලට දෙනවා.

yield Keyword එක පාවිච්චි කරලා තියෙන්නේ Fixture එක Test එකට Values දීලා, Test එක ඉවර වුණාම yield එකෙන් පස්සේ තියෙන කෝඩ් එක (cleanup කෝඩ් එක) Run කරන්නයි.

Fixtures වල Scope

Fixtures වලට විවිධ Scopes තියෙනවා. ඒ කියන්නේ Fixture එකක් Run වෙන්නේ කොයි වෙලාවටද, කොච්චර කල්ද ඒක Active වෙලා තියෙන්නේ කියන එක. මේක @pytest.fixture() Decorator එකට Argument එකක් විදියට දෙන්න පුළුවන්.

  • function (Default): හැම Test Function එකකටම කලින් Fixture එක Run වෙනවා.
  • class: Class එකක තියෙන හැම Test Method එකකටම කලින් එක වරක් Run වෙනවා.
  • module: File එකක තියෙන හැම Test එකකටම කලින් එක වරක් Run වෙනවා.
  • session: හැම Tests Collection එකකටම කලින් එක වරක් Run වෙනවා (මුළු Test Session එකටම).

session Scope එක Database Connection වගේ දේවල් වලට ගොඩක් ප්‍රයෝජනවත්. ඒකෙන් Test Run වෙන හැම වෙලාවෙම අලුතෙන් Connection එකක් හදන එක වළක්වා ගන්න පුළුවන්.

Parametrized Testing: එකම Test එකෙන් ගොඩක් දේවල් බලමු (Parametrized Testing: Let's Check Many Things with One Test)

ඔයාට එකම Test Logic එකක්, වෙනස් වෙනස් Input Values සෙට් එකක් එක්ක Test කරන්න ඕන වුණොත් මොකද කරන්නේ? උදාහරණයක් විදියට, add Function එක විවිධ Positive, Negative, Zero, Large Numbers එක්ක Test කරන්න ඕනේ. සාමාන්‍යයෙන් නම් ඔයාට ඒ හැම Input සෙට් එකකටම වෙන වෙනම Test Function එකක් ලියන්න වෙනවා.

ඒත් Pytest වල තියෙන @pytest.mark.parametrize කියන Decorator එකෙන් මේ දේ ගොඩක් ලේසියෙන් කරගන්න පුළුවන්. මේකෙන් එකම Test Function එකක්, දීලා තියෙන Inputs ටිකට අදාළව ගොඩක් වාරයක් Run වෙනවා.

@pytest.mark.parametrize පාවිච්චි කරන්නේ කොහොමද?

@pytest.mark.parametrize එකට Argument දෙකක් දෙනවා:

  1. පළවෙනි Argument එක: Test Function එකට යන Arguments වල නම් (String එකක් විදියට, comma වලින් වෙන් කරලා).
  2. දෙවෙනි Argument එක: Test කරන්න ඕනේ Data සෙට් එක (List of Tuples). හැම Tuple එකකම පළවෙනි Argument එකට අදාළ Value ටික පිළිවෙලට තියෙන්න ඕනේ.

අපි ආයෙත් add Function එකම උදාහරණයට ගමු.

test_calculator.py File එක (Parametrized Test එකක් සමඟ):

# test_calculator.py (Parametrized Example)
import pytest
from calculator import add

@pytest.mark.parametrize("a, b, expected_result", [
    (2, 3, 5),          # Positive numbers
    (-1, 1, 0),         # Mixed numbers
    (0, 0, 0),          # Zeros
    (100, -50, 50),     # Large positive and negative
    (-10, -20, -30)     # Negative numbers
])
def test_add_function_various_inputs(a, b, expected_result):
    actual_result = add(a, b)
    assert actual_result == expected_result

මේ Test එක Run කළොත්, Pytest විසින් මේ test_add_function_various_inputs කියන Function එක, උඩ දීලා තියෙන Inputs සෙට් පහටම වෙන වෙනම Run කරනවා. ඒ කියන්නේ එක Test Function එකක් තිබ්බත්, Test Reports වලට මේක Test 5ක් විදියට පෙන්නනවා. මේකෙන් කෝඩ් එක අඩු වෙනවා විතරක් නෙවෙයි, කියවන්නත්, Maintain කරන්නත් ගොඩක් පහසුයි.

ප්‍රායෝගික උදාහරණ: කලින් Test Pytest වලට හරවමු (Practical Examples: Let's Convert Previous Tests to Pytest)

හරි, දැන් අපි Pytest වල Theory එක තේරුම් ගත්තා. අපි හිතමු ඔයාට කලින් Python වල unittest Module එකෙන් ලියපු Test කෝඩ් එකක් තියෙනවා කියලා. ඒක Pytest වලට හරවන්නේ කොහොමද කියලා බලමු. මේකෙන් Pytest වල සරල බව තව තවත් පැහැදිලි වෙයි.

අපි පොඩි String Manipulation Module එකක් හදමු.

string_utils.py File එක:

# string_utils.py
def reverse_string(s):
    return s[::-1]

def is_palindrome(s):
    # Palindromes are case-insensitive and ignore spaces
    s = s.replace(" ", "").lower()
    return s == s[::-1]

unittest Module එකෙන් ලියපු Tests:

මේ උදාහරණ ටික අපි string_utils.py File එක Test කරන්න unittest Module එකෙන් ලියපු Test කියලා හිතමු.

# test_string_utils_old.py (Using unittest)
import unittest
from string_utils import reverse_string, is_palindrome

class TestStringUtils(unittest.TestCase):

    def test_reverse_string_basic(self):
        self.assertEqual(reverse_string("hello"), "olleh")

    def test_reverse_string_empty(self):
        self.assertEqual(reverse_string(""), "")

    def test_reverse_string_with_spaces(self):
        self.assertEqual(reverse_string("hello world"), "dlrow olleh")

    def test_is_palindrome_basic(self):
        self.assertTrue(is_palindrome("madam"))

    def test_is_palindrome_with_spaces_and_case(self):
        self.assertTrue(is_palindrome("Race Car"))

    def test_is_palindrome_not_palindrome(self):
        self.assertFalse(is_palindrome("python"))

    def test_is_palindrome_empty(self):
        self.assertTrue(is_palindrome(""))

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

Pytest වලට Convert කරපු Tests:

දැන් අපි මේ Tests ටික Pytest වලට හරවමු. බලන්න මේක කොච්චර සරල වෙනවද කියලා.

# test_string_utils.py (Using pytest)
import pytest
from string_utils import reverse_string, is_palindrome

def test_reverse_string_basic():
    assert reverse_string("hello") == "olleh"

def test_reverse_string_empty():
    assert reverse_string("") == ""

def test_reverse_string_with_spaces():
    assert reverse_string("hello world") == "dlrow olleh"

@pytest.mark.parametrize("input_string, expected_output", [
    ("madam", True),
    ("Race Car", True),
    ("python", False),
    ("", True), # An empty string can be considered a palindrome
    ("A man a plan a canal Panama", True)
])
def test_is_palindrome_various_inputs(input_string, expected_output):
    assert is_palindrome(input_string) == expected_output

දැන් බලන්න, unittest.TestCase Class එක inherit කරන්න ඕනේ නැහැ. self.assertEqual(), self.assertTrue() වගේ Method වෙනුවට, සරල assert Statement එක පාවිච්චි කරන්න පුළුවන්. is_palindrome Test එකට අපි @pytest.mark.parametrize පාවිච්චි කරලා, එක Test Function එකකින්ම Inputs ගොඩක් Test කරලා තියෙනවා. මේකෙන් Test කෝඩ් එකේ දිග අඩු වෙනවා වගේම, කියවන්නත් ලේසියි, නඩත්තු කරන්නත් පහසුයි.

Database Connection එකකට Fixture එකක්

අපි තව පොඩි උදාහරණයක් බලමු. අපි හිතමු ඔයාට Test කරන්න Database එකකට Connect වෙන්න ඕනේ කියලා. මේකට අපි conftest.py කියන File එක පාවිච්චි කරමු. conftest.py කියන්නේ Shared Fixtures තියෙන File එකක්. මේ File එක Project එකේ ඕනෑම Test File එකකට Automaticallyම Load වෙනවා.

conftest.py File එක (Project Root එකේ):

# conftest.py
import pytest

@pytest.fixture(scope="session") # මේ Fixture එක මුළු Test Session එකටම එක වරක් විතරයි run වෙන්නේ
def db_connection():
    print("\n--- Establishing database connection ---")
    # මෙතන Database connection එකක් හදනවා කියලා හිතන්න
    db = {"status": "connected", "data": []} # Dummy DB object
    yield db # Test වලට DB object එක දෙනවා
    print("--- Closing database connection ---")
    # Test session එක ඉවර වුණාම DB connection එක close කරනවා

test_db_operations.py File එක:

# test_db_operations.py
def test_add_data_to_db(db_connection):
    print("Test: Adding data...")
    db_connection["data"].append("new_item_1")
    assert "new_item_1" in db_connection["data"]

def test_retrieve_data_from_db(db_connection):
    print("Test: Retrieving data...")
    # Add some data first if not already present from other tests
    if not db_connection["data"]:
        db_connection["data"].append("initial_item")
    assert "initial_item" in db_connection["data"]
    assert len(db_connection["data"]) > 0

මේ උදාහරණයේදී, db_connection කියන Fixture එක session Scope එකකින් Run වෙනවා. ඒ කියන්නේ, ඔයා pytest Command එකෙන් Test Run කළාම, මුළු Test Session එකටම එක වරක් විතරයි Database Connection එක හැදෙන්නේ. මේක Resource Intensive Operations වලට (Database, Network Calls) ගොඩක් වැදගත්. Test Functions වලට db_connection කියන Argument එක දීලා, ඒක පාවිච්චි කරන්න පුළුවන්.

ස්ථාපනය, භාවිතය සහ Best Practices (Installation, Usage, and Best Practices)

Pytest ස්ථාපනය කරන්නේ කොහොමද? (How to Install Pytest?)

Pytest Install කරන එක ගොඩක් ලේසියි. ඔයාට පුළුවන් Pip පාවිච්චි කරලා Install කරන්න.

pip install pytest

Installation එක හරියට වුණාද කියලා බලන්න මේ Command එක Run කරන්න:

pytest --version

ඒකෙන් ඔයාට Pytest Version එක පෙන්නයි.

Tests Run කරන්නේ කොහොමද? (How to Run Tests?)

Pytest Install කරපු ගමන්, ඔයාට පුළුවන් Project එකේ Root Directory එකට ගිහින් Command Line එකේ pytest කියලා Type කරන්න. Pytest ස්වයංක්‍රීයවම Test Files හොයාගෙන Run කරයි.

cd your_project_directory
pytest

වැදගත් Options ටිකක්:

  • pytest -v: Verbose mode. Test එකේ නම් එක්ක, ඒක Pass ද Fail ද කියලා පැහැදිලිව පෙන්නනවා.
  • pytest -s: Print statements (print()) Tests Run වෙනකොට Console එකේ පෙන්නන්න.
  • pytest -k "name": නමෙන් Test Filter කරන්න. උදා: pytest -k "palindrome"
  • pytest path/to/your_test_file.py: නිශ්චිත Test File එකක් විතරක් Run කරන්න.
  • pytest path/to/your_test_file.py::test_specific_function: නිශ්චිත Test Function එකක් විතරක් Run කරන්න.

Assertions (තහවුරු කිරීම්)

අපි කලින් කතා කළා වගේ, Pytest වලට සාමාන්‍ය Python assert Statement එක පාවිච්චි කරන්න පුළුවන්. Pytest මේ assert Statement එක Fail වුණොත්, ඒක Fail වුණේ ඇයි, Values මොනවද වගේ දේවල් ගොඩක් පැහැදිලිව පෙන්නනවා. මේක Test Debug කරන එකට ලොකු පහසුවක්.

Exception (Error) එකක් Test කරන්න ඕන වුණොත්, Pytest වල pytest.raises කියන Context Manager එක පාවිච්චි කරන්න පුළුවන්. උදාහරණයක් විදියට:

import pytest

def divide(a, b):
    if b == 0:
        raise ValueError("Cannot divide by zero!")
    return a / b

def test_divide_by_zero():
    with pytest.raises(ValueError) as excinfo:
        divide(10, 0)
    assert "Cannot divide by zero!" in str(excinfo.value)

Best Practices (හොඳම පුරුදු)

Pytest පාවිච්චි කරනකොට මේ දේවල් මතක තියාගන්න එක ගොඩක් වැදගත්:

  1. Test Files නම් කිරීම: හැම Test File එකක්ම test_ වලින් පටන් ගන්න, නැත්නම් _test.py වලින් ඉවර කරන්න. (උදා: test_functions.py, calculator_test.py).
  2. Test Functions නම් කිරීම: හැම Test Function එකක්ම test_ වලින් පටන් ගන්න. (උදා: test_addition()).
  3. Tests ස්වාධීනව තබා ගන්න: හැම Test එකක්ම අනිත් Test එකකට බලපාන්නේ නැති විදියට තියෙන්න ඕනේ. එක Test එකක් Fail වුණා කියලා අනිත් ඒවා Fail වෙන්න හොඳ නැහැ.
  4. Fixtures බුද්ධිමත්ව පාවිච්චි කරන්න: Setup/Teardown logic එකට Fixtures පාවිච්චි කරන්න. ඒකෙන් කෝඩ් එක අඩු වෙනවා වගේම, Test එකේ Clarity එකත් වැඩි වෙනවා.
  5. conftest.py පාවිච්චි කරන්න: Project එකේ පුරාම shared Fixtures තියෙනවා නම්, ඒවා conftest.py File එකේ තියන්න.
  6. CI/CD එක්ක Integrat කරන්න: Pytest Tests, Your Continuous Integration/Continuous Delivery (CI/CD) Pipeline එකට අනිවාර්යයෙන්ම දාගන්න. එතකොට Code Push කරන හැම වෙලාවෙම Tests Run වෙලා, ඉක්මනින් වැරදි අඳුනගන්න පුළුවන්.

ඉතින් යාලුවනේ, ඔයාලට දැන් Pytest කියන්නේ මොකක්ද, ඒකේ තියෙන වාසි මොනවද, Fixtures සහ Parameterized Testing වගේ දේවල් කොහොමද පාවිච්චි කරන්නේ කියලා හොඳ අවබෝධයක් ලැබෙන්න ඇති කියලා මම හිතනවා. Pytest කියන්නේ Python Projects වලට Test ලියන්න තියෙන සුපිරිම Tool එකක්. ඒකෙන් අපේ කෝඩ් Quality එක වැඩි කරගන්න පුළුවන් වගේම, Development Process එකත් ගොඩක් වේගවත් කරගන්න පුළුවන්.

මතක තියාගන්න, හොඳින් Test කරපු කෝඩ් එකක් කියන්නේ විශ්වාසවන්ත, Error අඩු කෝඩ් එකක්. ඒකෙන් ඔයාටත් ඔයාගේ Team එකටත්, ඒ වගේම අවසානයේදී ඔයාගේ Product එක පාවිච්චි කරන අයටත් ලොකු පහසුවක් වෙනවා. Pytest පාවිච්චි කරලා බලන්න, ඒකෙන් ඔයාගේ Testing Experience එක ගොඩක් ලේසි වෙයි!

ඔබේ අත්දැකීම් පහළින් Comment කරන්න! Pytest ගැන ඔයාට තියෙන ප්‍රශ්න, නැත්නම් ඔයා දන්න Tips, Tricks මොනවද කියලා අපිත් එක්ක බෙදාගන්න. ඔයාගේ ඊළඟ Project එකට Pytest පාවිච්චි කරලා බලන්න!