Chapterبەش 3 · Lectureوانە 3.2

Automated Testing: Code That Checks Your Codeتاقیکردنەوەی خۆکار: کۆدێک کە کۆدەکەت دەپشکنێت

Why teams automate tests (a regression in Bazaar Cart 1.0.1), arrange–act–assert, Python's unittest (test cases, assertions, running and reading failures), choosing inputs by partitions and boundaries, fixtures and test discovery, the test pyramid with a real SQLite integration test, test doubles and unittest.mock, regression tests, test-driven development for rule R5, code coverage and doctest, pytest, continuous integration with a GitHub Actions workflow, tests in Java, Dart and PHP, and the common mistakes — all with real output.

بۆچی تیمەکان بەکار دەهێنن (پاشەکشەیەک لە ⁦Bazaar Cart 1.0.1⁩)، ئامادەکردن–جێبەجێکردن–دڵنیابوونەوە، unittestـی Python (، ، جێبەجێکردن و خوێندنەوەی سەرنەکەوتنەکان)، هەڵبژاردنی بە بەش و سنوور، و دۆزینەوەی تاقیکردنەوەکان، لەگەڵ لەسەر ڕاستەقینە، جێگرەوەکانی تاقیکردنەوە و ⁦unittest.mock⁩، ، بۆ یاسای ⁦R5⁩، و doctest، pytest، بە workflowـێکی GitHub Actions، تاقیکردنەوە لە Java، Dart و PHP و هەڵە باوەکان — هەموو بە دەرخستەی ڕاستەقینە.

24 min read23 خولەک خوێندنەوە · 21 stepsهەنگاو

What you will learnچی فێر دەبیت

  • Write unittest test cases with arrange–act–assert, run them and read a failure report.
  • Choose test inputs with partitions and edge cases, and organise tests with fixtures and discovery.
  • Explain the test pyramid and write an integration test and a test with a mock.
  • Use regression tests and the red–green–refactor loop of TDD; explain what coverage does and does not prove.
  • Describe continuous integration and read a GitHub Actions workflow that runs the tests on every pull request.
  • unittest بە ئامادەکردن–جێبەجێکردن–دڵنیابوونەوە بنووسیت، جێبەجێیان بکەیت و ڕاپۆرتی سەرنەکەوتن بخوێنیتەوە.
  • تاقیکردنەوە بە بەش و هەڵبژێریت و تاقیکردنەوەکان بە و دۆزینەوە ڕێک بخەیت.
  • ڕوون بکەیتەوە و و تاقیکردنەوەیەک بە Mock بنووسیت.
  • و سووڕی سوور–سەوز–ڕێکخستنەوەی TDD بەکار بهێنیت؛ ڕوون بکەیتەوە داپۆشین چی دەسەلمێنێت و چی ناسەلمێنێت.
  • وەسف بکەیت و workflowـێکی GitHub Actions بخوێنیتەوە کە تاقیکردنەوەکان لەسەر هەموو جێبەجێ دەکات.

A fix that broke somethingچاکسازییەک کە شتێکی تێکدا

Soran writes on the team chat: version 1.0.0 charges 2,750 IQD for 350 g of sumac — it should be 3,000. Rule R1 says every line is rounded up to the next 250 IQD, but the code rounded down. (All prices in this course are made up, in IQD.) Shilan fixes it in two minutes, tries the sumac order once by hand, sees 3,000 and is happy:

سۆران لە چاتی تیمەکەدا دەنووسێت: ⁦1.0.0⁩ بۆ ⁦350⁩ گرام سوماق ⁦2,750⁩ دینار وەردەگرێت — دەبێت ⁦3,000⁩ بێت. یاسای ⁦R1⁩ دەڵێت هەموو دێڕێک بەرەو سەرەوە نزیک دەکرێتەوە بۆ ⁦250⁩ دیناری دواتر، بەڵام کۆدەکە بەرەو خوارەوە نزیکی دەکردەوە. (هەموو نرخەکانی ئەم خولە دروستکراون، بە دینار.) شیلان لە دوو خولەکدا چاکی دەکاتەوە، یەک جار بە دەست داواکاریی سوماق تاقی دەکاتەوە، ⁦3,000⁩ دەبینێت و دڵخۆشە:

Shilan's quick fix in prices.pyچاکسازییە خێراکەی شیلان لە ⁦prices.py⁩PYTHON
def round_up_250(amount):
    """Round an amount up to the next 250 IQD."""
    return amount // 250 * 250 + 250  # Shilan's quick fix
Checking by hand: sumac looks right… and walnuts?پشکنین بە دەست: سوماق ڕاست دەردەکەوێت… و گوێز؟BASH
$ python3.13 -c "from prices import line_price; print(line_price(8000, 350)); print(line_price(18000, 500))"
3000.0
9250.0

Sumac is right now, but 500 g of walnuts — exactly 9,000 IQD, which needed no rounding — has become 9,250. The fix broke something that used to work: a regression. Nobody tried walnuts, because checking every case by hand after every change is slow and boring. The answer is code that checks code: automated tests. The four levels of testing are in What is programming?; this lesson shows how to write and run the tests themselves.

ئێستا سوماق ڕاستە، بەڵام ⁦500⁩ گرام گوێز — ڕێک ⁦9,000⁩ دینار کە پێویستی بە نزیککردنەوە نەبوو — بووە بە ⁦9,250⁩. چاکسازییەکە شتێکی تێکدا کە پێشتر کاری دەکرد: پاشەکشەیەک. کەس گوێزی تاقی نەکردەوە، چونکە پشکنینی هەموو حاڵەتێک بە دەست دوای هەموو گۆڕانکارییەک خاو و بێزارکەرە. چارەسەرەکە کۆدێکە کە کۆد دەپشکنێت: . چوار ئاستەکەی لە بەرنامەسازی چییە؟ـدان؛ ئەم وانەیە پیشان دەدات خودی تاقیکردنەوەکان چۆن دەنووسرێن و جێبەجێ دەکرێن.

Two price tags side by side. Version 1.0.0: sumac 350 g 2,750 IQD (wrong, crossed), walnuts 500 g 9,000 IQD (right, ticked). After Shilan's quick fix: sumac 3,000 (right, ticked), walnuts 9,250 (wrong, crossed). A note says the fix repaired one case and broke another that used to work: a regression.دوو تابلۆی نرخ لە تەنیشت یەک. وەشانی 1.0.0: سوماق 350 گرام 2,750 دینار (هەڵە، کوژێنراوەتەوە)، گوێز 500 گرام 9,000 دینار (ڕاست، نیشانکراو). دوای چاکسازییە خێراکەی شیلان: سوماق 3,000 (ڕاست)، گوێز 9,250 (هەڵە). تێبینییەک دەڵێت چاکسازییەکە حاڵەتێکی چاک کرد و یەکێکی تری تێکدا کە پێشتر کاری دەکرد: پاشەکشە.
Fixing one case, breaking another: a regression.چاککردنی حاڵەتێک و تێکدانی یەکێکی تر: پاشەکشە.CêIT

Your first test: arrange, act, assertیەکەم تاقیکردنەوەت: ئامادەکردن، جێبەجێکردن، دڵنیابوونەوە

An automated test is a small program that calls the real code with an input you chose and compares the answer with the answer you expect. Most tests have three parts, called arrange–act–assert: prepare the input, run the code, check the result. The check is an assertion; Python's assert statement from Checking your own assumptions is enough for a first try:

بچووکە کە کۆدی ڕاستەقینە بە کە خۆت هەڵتبژاردووە بانگ دەکات و وەڵامەکە لەگەڵ ئەو وەڵامەی چاوەڕێی دەکەیت بەراورد دەکات. زۆربەی تاقیکردنەوەکان سێ بەشیان هەیە، پێیان دەوترێت ⁦arrange–act–assert⁩: ئامادە بکە، کۆدەکە جێبەجێ بکە، ئەنجامەکە بپشکنە. پشکنینەکە ـە؛ ڕستەی assertـی Python لە پشکنینی گریمانەکانی خۆت بۆ یەکەم هەوڵ بەسە:

A first check with plain assertیەکەم پشکنین بە assertـی سادەcheck_prices.pyPYTHON
"""A first automated check with plain assert."""
from prices import line_price

# Arrange: the input and the answer we expect
price_per_kg, grams, expected = 8000, 350, 3000

# Act: run the code
result = line_price(price_per_kg, grams)

# Assert: compare the result with the expected answer
assert result == expected, f"sumac: got {result}, expected {expected}"
assert line_price(18000, 500) == 9000, "walnuts 500 g must stay 9,000"
print("All checks passed.")
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 4-5

Arrange: the input and the expected answer.

ئامادەکردن: و وەڵامی چاوەڕوانکراو.

Lineدێڕی 7-8

Act: call the real code.

جێبەجێکردن: کۆدی ڕاستەقینە بانگ بکە.

Lineدێڕی 10-12

Assert: if a condition is false, Python stops with AssertionError and your message.

دڵنیابوونەوە: ئەگەر مەرجێک هەڵە بێت، Python بە AssertionError و پەیامەکەت ڕادەوەستێت.

Run against Shilan's quick fix, the second check fails at once and names the walnuts. With the correct fix (math.ceil) it prints All checks passed. — in less than a second, as often as you like.

لەسەر چاکسازییە خێراکەی شیلان، پشکنینی دووەم یەکسەر سەرناکەوێت و ناوی گوێز دەبات. بە چاکسازییە دروستەکە (math.ceil) All checks passed. چاپ دەکات — لە کەمتر لە چرکەیەکدا، هەر چەند جار بتەوێت.

Three boxes in a row. Arrange: price_per_kg 8000, grams 350, expected 3000. Act: result = line_price(8000, 350). Assert: result == expected? A green tick for yes (the test passes) and a red cross for no (the test fails and shows both values).سێ چوارگۆشە لە ڕیزێکدا. ئامادەکردن: price_per_kg 8000، grams 350، expected 3000. جێبەجێکردن: result = line_price(8000, 350). دڵنیابوونەوە: result == expected؟ نیشانەیەکی سەوز بۆ بەڵێ (تاقیکردنەوەکە سەردەکەوێت) و خاچێکی سوور بۆ نەخێر (سەرناکەوێت و هەردوو بەهاکە پیشان دەدات).
Every test: arrange, act, assert.هەموو تاقیکردنەوەیەک: ئامادەکردن، جێبەجێکردن، دڵنیابوونەوە.CêIT

unittest: test cases and assertionsunittest: حاڵەتەکانی تاقیکردنەوە و دڵنیاکردنەوەکان

Plain assert stops at the first failure and gives no summary. Python's standard library has a full testing framework, unittest (inspired by Java's JUnit). You write a class that inherits from unittest.TestCase; every method whose name starts with test is one test case; the self.assert… methods are the assertions. All the test cases together form a test suite.

assertـی سادە لە یەکەم سەرنەکەوتندا ڕادەوەستێت و هیچ پوختەیەک نادات. ستانداردی Python تەواوی تاقیکردنەوەی هەیە، unittest (کە لە JUnitـی Javaـەوە ئیلهامی وەرگرتووە). کلاسێک دەنووسیت کە لە unittest.TestCase بۆماوە وەردەگرێت؛ هەر کە ناوەکەی بە test دەست پێبکات یەک ـە؛ self.assert… . هەموو پێکەوە پێکدەهێنن.

Five test cases for prices.pyپێنج حاڵەتی تاقیکردنەوە بۆ ⁦prices.py⁩test_prices.pyPYTHON
import unittest

from prices import delivery_fee, line_price


class LinePriceTest(unittest.TestCase):
    def test_sumac_350_g_is_rounded_up(self):
        self.assertEqual(line_price(8000, 350), 3000)

    def test_exact_price_stays_the_same(self):
        self.assertEqual(line_price(18000, 500), 9000)

    def test_less_than_100_g_is_refused(self):
        with self.assertRaises(ValueError):
            line_price(8000, 99)


class DeliveryFeeTest(unittest.TestCase):
    def test_small_order_pays_3000(self):
        self.assertEqual(delivery_fee(49_750), 3000)

    def test_free_from_50000(self):
        self.assertEqual(delivery_fee(50_000), 0)


if __name__ == "__main__":
    unittest.main()
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 7

One test case. The long name says what is checked; it appears in the report.

یەک . ناوە درێژەکە دەڵێت چی دەپشکنرێت؛ لە ڕاپۆرتەکەدا دەردەکەوێت.

Lineدێڕی 13-15

assertRaises passes only if the code inside raises ValueError (rule R2: at least 100 g).

assertRaises تەنیا ئەو کاتە سەردەکەوێت کە کۆدی ناوەوە ValueError بەرز بکاتەوە (یاسای ⁦R2⁩: لانیکەم ⁦100⁩ گرام).

Run the suite with -v (verbose)جێبەجێکردنی کۆمەڵەکە بە -v (ورد)BASH
$ python3.13 -m unittest -v test_prices
test_free_from_50000 (test_prices.DeliveryFeeTest.test_free_from_50000) ... ok
test_small_order_pays_3000 (test_prices.DeliveryFeeTest.test_small_order_pays_3000) ... ok
test_exact_price_stays_the_same (test_prices.LinePriceTest.test_exact_price_stays_the_same) ... ok
test_less_than_100_g_is_refused (test_prices.LinePriceTest.test_less_than_100_g_is_refused) ... ok
test_sumac_350_g_is_rounded_up (test_prices.LinePriceTest.test_sumac_350_g_is_rounded_up) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
The test file with labels: class LinePriceTest(unittest.TestCase) is a group of test cases; def test_sumac_350_g_is_rounded_up(self) is one test case, because its name starts with test; self.assertEqual(line_price(8000, 350), 3000) is the assertion, actual value then expected value; with self.assertRaises(ValueError) expects an exception.فایلی تاقیکردنەوەکە بە ڕوونکردنەوە: class LinePriceTest(unittest.TestCase) گرووپێکی حاڵەتەکانی تاقیکردنەوەـە؛ def test_sumac_350_g_is_rounded_up(self) یەک حاڵەتی تاقیکردنەوەـە، چونکە ناوەکەی بە test دەست پێدەکات؛ self.assertEqual(line_price(8000, 350), 3000) دڵنیاکردنەوەکەیە، بەهای ڕاستەقینە پاشان چاوەڕوانکراو؛ with self.assertRaises(ValueError) چاوەڕێی هەڵاوێردەیەک دەکات.
The parts of a unittest file.بەشەکانی unittest.CêIT

Reading a failing testخوێندنەوەی تاقیکردنەوەیەکی سەرنەکەوتوو

Now run the same suite against Shilan's quick fix. Read the report from the top: one character per test (. passed, F failed, E error), then one block per problem, then the summary:

ئێستا هەمان کۆمەڵە لەسەر چاکسازییە خێراکەی شیلان جێبەجێ بکە. ڕاپۆرتەکە لە سەرەوە بخوێنەوە: بۆ هەر تاقیکردنەوەیەک یەک پیت (. سەرکەوت، F سەرنەکەوت، E هەڵە)، پاشان بۆ هەر کێشەیەک بلۆکێک، پاشان پوختەکە:

The suite catches the regressionکۆمەڵەکە پاشەکشەکە دەگرێتBASH
$ python3.13 -m unittest test_prices
..F..
======================================================================
FAIL: test_exact_price_stays_the_same (test_prices.LinePriceTest.test_exact_price_stays_the_same)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/se/bazaar-cart/test_prices.py", line 11, in test_exact_price_stays_the_same
    self.assertEqual(line_price(18000, 500), 9000)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 9250.0 != 9000

----------------------------------------------------------------------
Ran 5 tests in 0.001s

FAILED (failures=1)
The failing report with labels: ..F.. shows five tests and the third failed; FAIL names the test and its class; the traceback line gives file test_prices.py and line 11; AssertionError: 9250.0 != 9000 shows what the code returned and what the test expected; FAILED (failures=1) is the summary.ڕاپۆرتە سەرنەکەوتووەکە بە ڕوونکردنەوە: ..F.. پێنج تاقیکردنەوە پیشان دەدات و سێیەمیان سەرنەکەوت؛ FAIL ناوی تاقیکردنەوەکە و کلاسەکەی دەبات؛ دێڕی ڕێچکەی هەڵە فایلی test_prices.py و دێڕی 11 دەدات؛ AssertionError: 9250.0 != 9000 پیشان دەدات کۆدەکە چی گەڕاندەوە و تاقیکردنەوەکە چاوەڕێی چی دەکرد؛ FAILED (failures=1) پوختەکەیە.
Where to look in a failure report.لە ڕاپۆرتی سەرنەکەوتندا سەیری کوێ بکەیت.CêIT

F means an assertion was false: the code gave a wrong answer. E means the test itself crashed with another exception (for example a typo, or an import that fails) before it could check anything. The line 9250.0 != 9000 is the most useful one: the first number is what line_price returned, the second is what the test expected. How to read a traceback in general is in Reading a traceback.

F واتە هەڵە بوو: کۆدەکە وەڵامێکی هەڵەی دا. E واتە خودی تاقیکردنەوەکە بە تر تێکچوو (بۆ نموونە هەڵەیەکی نووسین، یان importـێک کە سەرناکەوێت) پێش ئەوەی بتوانێت هیچ شتێک بپشکنێت. دێڕی 9250.0 != 9000 بەسوودترینیانە: ژمارەی یەکەم ئەوەیە کە line_price گەڕاندوویەتەوە، دووەم ئەوەیە کە تاقیکردنەوەکە چاوەڕێی دەکرد. خوێندنەوەی بە گشتی لە خوێندنەوەی ڕێچکەی هەڵەدایە.

Which inputs? Partitions and boundariesکام نێوخستەکان؟ بەشەکان و سنوورەکان

You cannot test every weight from 100 g to 10,000 g in every combination. MIT's software course gives a method: split the inputs into groups that should behave the same (partitions), take one value from each group, and add the values on the edges between groups, because bugs hide there — > written instead of >=, a loop that stops one step early. Such values are called edge cases. For Bazaar Cart: weights 99 | 100 … 10,000 | 10,050 g; goods totals 49,750 | 50,000 | 50,250 IQD. subTest runs a whole table of cases in one test and names the case that fails:

ناتوانیت هەموو کێشێک لە ⁦100⁩ گرامەوە تا ⁦10,000⁩ گرام بە هەموو تێکەڵەیەک تاقی بکەیتەوە. خولی MIT ڕێگایەک دەدات: دابەش بکە بۆ گرووپی وا کە دەبێت هەمان ڕەفتاریان هەبێت (بەشەکان)، لە هەر گرووپێک بەهایەک وەربگرە و ئەو بەهایانەی سەر سنووری نێوان گرووپەکان زیاد بکە، چونکە لەوێ خۆیان دەشارنەوە — > لەبری >= نووسراو، کە هەنگاوێک زووتر ڕادەوەستێت. بەم بەهایانە دەوترێت . بۆ Bazaar Cart: کێشەکان ⁦99 | 100 … 10,000 | 10,050⁩ گرام؛ کۆی کاڵاکان ⁦49,750 | 50,000 | 50,250⁩ دینار. subTest خشتەیەکی تەواو لە حاڵەتەکان لە یەک تاقیکردنەوەدا جێبەجێ دەکات و ناوی ئەو حاڵەتە دەبات کە سەرناکەوێت:

Boundary values in a tableبەهاکانی سنوور لە خشتەیەکداtest_boundaries.pyPYTHON
import unittest

from prices import delivery_fee, line_price


class BoundaryTest(unittest.TestCase):
    def test_weights_that_are_allowed(self):
        for grams, price in [(100, 1000), (350, 3000), (10_000, 80_000)]:
            with self.subTest(grams=grams):
                self.assertEqual(line_price(8000, grams), price)

    def test_weights_that_are_refused(self):
        for grams in [0, 99, 10_050]:
            with self.subTest(grams=grams):
                with self.assertRaises(ValueError):
                    line_price(8000, grams)

    def test_delivery_around_50000(self):
        for goods_total, fee in [(49_750, 3000), (50_000, 0), (50_250, 0)]:
            with self.subTest(goods_total=goods_total):
                self.assertEqual(delivery_fee(goods_total), fee)


if __name__ == "__main__":
    unittest.main()
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 8-10

Each row is labelled with subTest, so one failing row does not hide the others.

هەر ڕیزێک بە subTest ناونووس دەکرێت، بۆیە ڕیزێکی سەرنەکەوتوو ئەوانی تر ناشارێتەوە.

Karwan's PR from the GitHub lesson wrote goods_total > 50000. This suite catches it straight away, and even says which row:

PRـەکەی کاروان لە وانەی GitHub goods_total > 50000ـی نووسیبوو. ئەم کۆمەڵەیە یەکسەر دەیگرێت و تەنانەت دەڵێت کام ڕیز:

With > instead of >= in delivery_fee()بە > لەبری >= لە ⁦delivery_fee()⁩BASH
$ python3.13 -m unittest test_boundaries
F..
======================================================================
FAIL: test_delivery_around_50000 (test_boundaries.BoundaryTest.test_delivery_around_50000) (goods_total=50000)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/se/bazaar-cart/test_boundaries.py", line 21, in test_delivery_around_50000
    self.assertEqual(delivery_fee(goods_total), fee)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 3000 != 0

----------------------------------------------------------------------
Ran 3 tests in 0.001s

FAILED (failures=1)
Two number lines. Weight in grams: below 100 is refused, 100 to 10,000 is allowed, above 10,000 is refused; test points at 0, 99, 100, 350, 10,000 and 10,050, with the four edge points highlighted. Goods total in IQD: below 50,000 pays 3,000 delivery, from 50,000 delivery is free; test points 49,750, 50,000 and 50,250, with 50,000 highlighted as the value a > bug gets wrong.دوو هێڵی ژمارە. کێش بە گرام: کەمتر لە 100 ڕەتدەکرێتەوە، 100 تا 10,000 ڕێگەپێدراوە، زیاتر لە 10,000 ڕەتدەکرێتەوە؛ خاڵەکانی تاقیکردنەوە 0، 99، 100، 350، 10,000 و 10,050، چوار خاڵی سنوور ڕووناک کراون. کۆی کاڵاکان بە دینار: کەمتر لە 50,000 گەیاندنی 3,000ـە، لە 50,000ـەوە گەیاندن بێبەرانبەرە؛ خاڵەکان 49,750، 50,000 و 50,250، 50,000 ڕووناک کراوە وەک ئەو بەهایەی باگێک بە > هەڵەی تێدا دەکات.
Test one value per group, plus the values on each edge.لە هەر گرووپێک بەهایەک تاقی بکەرەوە، لەگەڵ بەهاکانی سەر هەر سنوورێک.CêIT

Fixtures, discovery and folder layoutئامادەکاریی تاقیکردنەوە، دۆزینەوە و ڕێکخستنی فۆڵدەرەکان

Real projects keep tests in a tests/ folder, one file per module, each name starting with test. The folder needs an empty __init__.py. Then python3.13 -m unittest with no file names discovers every test*.py below the current folder and runs them all. Many tests need something prepared first — a fresh database, a fake list of products. That preparation is a test fixture: setUp() runs before each test and tearDown() after it, so every test starts clean and no test depends on another.

پڕۆژە ڕاستەقینەکان تاقیکردنەوەکان لە tests/ـدا هەڵدەگرن، بۆ هەر و هەر ناوێک بە test دەست پێدەکات. پێویستی بە __init__.pyـێکی بەتاڵە. پاشان python3.13 -m unittest بەبێ ناوی هەموو test*.pyـێک لە ژێر ئێستادا دەدۆزێتەوە و هەموویان جێبەجێ دەکات. زۆر تاقیکردنەوە پێویستیان بە ئامادەکردنی شتێکە — نوێ، لیستێکی ساختەی کاڵا. ئەو ئامادەکارییە ـە: setUp() پێش هەر تاقیکردنەوەیەک جێبەجێ دەبێت و tearDown() دوای ئەو، بۆ ئەوەی هەموو تاقیکردنەوەیەک پاک دەست پێبکات و هیچ تاقیکردنەوەیەک پشت بە یەکێکی تر نەبەستێت.

The project with a tests/ folder, then discoveryپڕۆژەکە بە فۆڵدەری tests/، پاشان دۆزینەوەBASH
$ find . -name "*.py" | sort
./orders.py
./prices.py
./repository.py
./tests/__init__.py
./tests/fakes.py
./tests/test_boundaries.py
./tests/test_orders.py
./tests/test_prices.py
./tests/test_repository.py
$ python3.13 -m unittest
..............
----------------------------------------------------------------------
Ran 14 tests in 0.001s

OK
Left: the folder tree bazaar-cart with prices.py, repository.py, orders.py and a tests folder holding __init__.py, fakes.py and four test_ files; an arrow says python3.13 -m unittest finds every test*.py. Right: a timeline for two tests, each wrapped in setUp before and tearDown after, so each test gets a fresh fixture.چەپ: داری فۆڵدەری bazaar-cart بە prices.py، repository.py، orders.py و فۆڵدەری tests کە __init__.py، fakes.py و چوار فایلی test_ـی تێدایە؛ تیرێک دەڵێت python3.13 -m unittest هەموو test*.pyـێک دەدۆزێتەوە. ڕاست: هێڵێکی کاتی بۆ دوو تاقیکردنەوە، هەریەکە لە نێوان setUp لە پێشەوە و tearDown لە دواوە، بۆیە هەر تاقیکردنەوەیەک ئامادەکارییەکی نوێ وەردەگرێت.
Tests live in tests/; setUp and tearDown wrap each test.تاقیکردنەوەکان لە tests/ـدان؛ setUp و tearDown هەر تاقیکردنەوەیەک دەپێچنەوە.CêIT

The test pyramidهەرەمی تاقیکردنەوە

Tests come in sizes. A unit test checks one small piece alone and runs in a millisecond. An integration test checks pieces working together — our code with a real database. An end-to-end test (a system test) drives the whole app like a user and is slow and fragile. Mike Cohn's test pyramid says: write many unit tests at the bottom, some integration tests in the middle and only a few end-to-end tests at the top. Martin Fowler's site warns about the upside-down ice-cream cone — mostly slow end-to-end tests — and adds: if a high-level test finds a bug that no unit test caught, add the missing unit test.

تاقیکردنەوەکان قەبارەیان هەیە. بەشێکی بچووک بە تەنیا دەپشکنێت و لە میلیچرکەیەکدا جێبەجێ دەبێت. بەشەکان پێکەوە دەپشکنێت — کۆدەکەمان لەگەڵ ڕاستەقینە. تاقیکردنەوەی سەرتاسەری (تاقیکردنەوەی سیستەم) هەموو ئەپەکە وەک بەکار دەهێنێت و خاو و ناسکە. ـی مایک کۆن دەڵێت: لە بنەوە زۆر بنووسە، لە ناوەڕاست هەندێک و لە سەرەوە تەنیا کەمێک تاقیکردنەوەی سەرتاسەری. ماڵپەڕەکەی مارتن فاولەر ئاگاداری قوچەکی بەستەنی پێچەوانە دەکاتەوە — زۆربەی تاقیکردنەوەکان سەرتاسەری و خاو — و زیاد دەکات: ئەگەر تاقیکردنەوەیەکی ئاستبەرز دۆزییەوە کە هیچ نەیگرت، ئەو کەمە زیاد بکە.

A pyramid in three layers. Bottom and widest: unit tests — many, fast (milliseconds), one function, example line_price(8000, 350). Middle: integration tests — some, slower, parts together, example order_total with a real SQLite database. Top and narrow: end-to-end tests — few, slow and fragile, the whole app like a user, example type an order in the app and read the receipt. Arrows at the side: up = more confidence per test, down = faster and cheaper.هەرەمێک بە سێ چین. بنەوە و پانترین: تاقیکردنەوەکانی یەکە — زۆر، خێرا (میلیچرکە)، یەک کردار، نموونە line_price(8000, 350). ناوەڕاست: تاقیکردنەوەی پێکەوەبەستن — هەندێک، خاوتر، بەشەکان پێکەوە، نموونە order_total لەگەڵ بنکەدراوەیەکی ڕاستەقینەی SQLite. سەرەوە و تەسک: تاقیکردنەوەی سەرتاسەری — کەم، خاو و ناسک، هەموو ئەپەکە وەک بەکارهێنەرێک، نموونە نووسینی داواکارییەک لە ئەپەکەدا و خوێندنەوەی پسوولەکە. تیرەکانی لاتەنیشت: بەرەو سەرەوە = متمانەی زیاتر بۆ هەر تاقیکردنەوەیەک، بەرەو خوارەوە = خێراتر و هەرزانتر.
Many small tests, fewer big ones.زۆر تاقیکردنەوەی بچووک، کەمتر لە گەورەکان.CêIT

Flutter uses the same shape with its own names — unit, widget and integration tests; the template's widget test is in Lint rules and the template test.

Flutter هەمان شێوە بە ناوی خۆی بەکار دەهێنێت — ، تاقیکردنەوەی و ؛ تاقیکردنەوەی قاڵبەکە لە یاساکانی lint و تاقیکردنەوەی قاڵبدایە.

The test pyramidهەرەمی تاقیکردنەوە

An integration test with SQLiteتاقیکردنەوەی پێکەوەبەستن لەگەڵ SQLite

order_total() adds the lines, checks the stock (rule R3) and adds delivery. Its prices come from a ProductRepository that reads an SQLite database; the full layered design comes in Layers and business logic. This integration test uses the real repository and a real SQLite database kept in memory (":memory:"), made fresh in setUp() — no files left behind:

order_total() دێڕەکان کۆ دەکاتەوە، کۆگا دەپشکنێت (یاسای ⁦R3⁩) و گەیاندن زیاد دەکات. نرخەکانی لە ProductRepositoryـەوە دێن کە SQLite دەخوێنێتەوە؛ دیزاینی تەواوی چینەکان لە چینەکان و لۆجیکی کاروبار دێت. ئەم کۆگای ڕاستەقینە و ڕاستەقینەی بەکار دەهێنێت کە لە (":memory:") هەڵدەگیرێت و لە setUp()ـدا نوێ دروست دەکرێت — هیچ بەجێ ناهێڵێت:

Integration test: order_total + ProductRepository + SQLiteتاقیکردنەوەی پێکەوەبەستن: ⁦order_total + ProductRepository +⁩ SQLitetests/test_repository.pyPYTHON
import sqlite3
import unittest

from orders import order_total
from repository import ProductRepository


class ProductRepositoryTest(unittest.TestCase):
    def setUp(self):
        # A new, empty database in memory for every test
        self.db = sqlite3.connect(":memory:")
        self.repo = ProductRepository(self.db)
        self.repo.add("sumac", 8000, 2000)
        self.repo.add("walnuts", 18000, 5000)

    def tearDown(self):
        self.db.close()

    def test_price_comes_back_from_the_database(self):
        self.assertEqual(self.repo.price_per_kg("sumac"), 8000)

    def test_unknown_product_raises_key_error(self):
        with self.assertRaises(KeyError):
            self.repo.price_per_kg("saffron")

    def test_order_total_with_the_real_database(self):
        lines = [("walnuts", 500), ("sumac", 350)]
        self.assertEqual(order_total(lines, self.repo), 15_000)
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 9-14

The fixture: a new, empty database in memory for every test, filled with two products (price per kg, stock in grams).

: نوێ و بەتاڵ لە بۆ هەموو تاقیکردنەوەیەک، بە دوو کاڵا پڕکراوە (نرخی کیلۆ، کۆگا بە گرام).

Lineدێڕی 26-28

Walnuts 500 g (9,000) + sumac 350 g (3,000) + delivery 3,000 = 15,000 IQD, read through real SQL.

گوێز ⁦500⁩ گرام ⁦(9,000) +⁩ سوماق ⁦350⁩ گرام ⁦(3,000) +⁩ گەیاندن ⁦3,000 = 15,000⁩ دینار، لە ڕێی SQLـی ڕاستەقینەوە خوێندراوەتەوە.

Running the integration tests and the order testsجێبەجێکردنی تاقیکردنەوەی پێکەوەبەستن و تاقیکردنەوەکانی داواکاریBASH
$ python3.13 -m unittest -v tests.test_repository tests.test_orders
test_order_total_with_the_real_database (tests.test_repository.ProductRepositoryTest.test_order_total_with_the_real_database) ... ok
test_price_comes_back_from_the_database (tests.test_repository.ProductRepositoryTest.test_price_comes_back_from_the_database) ... ok
test_unknown_product_raises_key_error (tests.test_repository.ProductRepositoryTest.test_unknown_product_raises_key_error) ... ok
test_confirm_sends_one_message (tests.test_orders.OrderTest.test_confirm_sends_one_message) ... ok
test_goods_plus_delivery (tests.test_orders.OrderTest.test_goods_plus_delivery) ... ok
test_more_than_the_stock_is_refused (tests.test_orders.OrderTest.test_more_than_the_stock_is_refused) ... ok

----------------------------------------------------------------------
Ran 6 tests in 0.001s

OK
Three rows showing what each kind of test touches. Unit test: order_total and a fake product list in a dictionary. Integration test: order_total, ProductRepository and a real SQLite database in memory. End-to-end test: a person at the shop app, the screen, order_total, the repository and the database, all together.سێ ڕیز کە پیشان دەدەن هەر جۆرە تاقیکردنەوەیەک دەست لە چی دەدات. تاقیکردنەوەی یەکە: order_total و لیستێکی ساختەی کاڵا لە فەرهەنگێکدا. تاقیکردنەوەی پێکەوەبەستن: order_total، ProductRepository و بنکەدراوەیەکی ڕاستەقینەی SQLite لە بیرگەدا. تاقیکردنەوەی سەرتاسەری: کەسێک لە ئەپی دوکانەکە، شاشەکە، order_total، کۆگاکە و بنکەدراوەکە، هەموویان پێکەوە.
The bigger the test, the more real parts it uses.تاقیکردنەوە تا گەورەتر بێت، بەشی ڕاستەقینەی زیاتر بەکار دەهێنێت.CêIT

Test doubles: stand-ins for the real thingجێگرەوەکانی تاقیکردنەوە: لەبری شتە ڕاستەقینەکە

Sometimes the real part is slow, costs money or does something you cannot undo — such as sending an SMS to a real customer. In a test you replace it with a stand-in, the way a film uses a stunt double. Gerard Meszaros named these test doubles, and Martin Fowler's site lists five kinds:

هەندێک جار بەشە ڕاستەقینەکە خاوە، پارەی تێدەچێت یان کارێک دەکات کە ناتوانیت بیگەڕێنیتەوە — وەک ناردنی بۆ کڕیارێکی ڕاستەقینە. لە تاقیکردنەوەدا جێگرەوەیەکی لەبری دادەنێیت، وەک ئەوەی لە فیلمدا کەسێکی تر لە دیمەنە مەترسیدارەکاندا لەبری ئەکتەرەکە یاری دەکات. جێرارد مێزارۆس ئەم جێگرەوانەی ناو نا جێگرەوەکانی تاقیکردنەوە ( بە مانا فراوانەکەی) و ماڵپەڕەکەی مارتن فاولەر پێنج جۆریان دەژمێرێت:

The five kinds of test double (Meszaros, via martinfowler.com).پێنج جۆرەکەی جێگرەوەی تاقیکردنەوە (مێزارۆس، لە ڕێی ⁦martinfowler.com⁩).
KindجۆرWhat it doesچی دەکاتBazaar Cart exampleنموونەی Bazaar Cart
Dummyis passed in but never usedدەدرێت بەڵام هەرگیز بەکار نایەتa notifier for a test that sends nothingnotifierـێک بۆ تاقیکردنەوەیەک کە هیچ نانێرێت
Stubreturns fixed answersوەڵامی جێگیر دەداتەوەprice_per_kg() always returns 8000price_per_kg() هەمیشە ⁦8000⁩ دەگەڕێنێتەوە
Fakereally works, but takes a shortcutبەڕاستی کار دەکات، بەڵام ڕێگایەکی کورت دەگرێتInMemoryProductRepository: a dictionary instead of a databaseInMemoryProductRepository: لەبری
Spya stub that also records its callsstubـێک کە بانگکردنەکانیشی تۆمار دەکاتa notifier that keeps a list of sent messagesnotifierـێک کە لیستی پەیامە نێردراوەکان هەڵدەگرێت
Mockis told which calls to expect and is checked at the endپێی دەوترێت چاوەڕێی چ بانگکردنێک بکات و لە کۆتاییدا دەپشکنرێت“send must be called once with this text”«send دەبێت یەک جار بەم دەقە بانگ بکرێت»
Five cards in a row, each with a small icon: Dummy (an empty box: passed in, never used), Stub (a speech bubble with 8000: fixed answers), Fake (a dictionary instead of a database: works, takes a shortcut), Spy (a notebook: records the calls), Mock (a checklist: expects calls, checked at the end). Above them: the real SMS notifier, crossed out for tests.پێنج کارت لە ڕیزێکدا، هەریەکە بە ئایکۆنێکی بچووک: Dummy (سندوقێکی بەتاڵ: دەدرێت، هەرگیز بەکار نایەت)، Stub (بڵقێکی قسە بە 8000: وەڵامی جێگیر)، Fake (فەرهەنگێک لەبری بنکەدراوە: کار دەکات، ڕێگای کورت دەگرێت)، Spy (دەفتەرێک: بانگکردنەکان تۆمار دەکات)، Mock (لیستێکی پشکنین: چاوەڕێی بانگکردن دەکات، لە کۆتاییدا دەپشکنرێت). لە سەرووی ئەمانەوە: notifierـی ڕاستەقینەی نامەی کورت، بۆ تاقیکردنەوە کوژێنراوەتەوە.
Five stand-ins, from "just fills a gap" to "checks every call".پێنج جێگرەوە، لە «تەنیا بۆشاییەک پڕ دەکاتەوە» تا «هەموو بانگکردنێک دەپشکنێت».CêIT

Test doubles: stand-ins for the real thingجێگرەوەکانی تاقیکردنەوە: لەبری شتە ڕاستەقینەکە

unittest.mock: was the message sent?⁦unittest.mock⁩: ئایا پەیامەکە نێردرا؟

confirm_order() works out the total and sends the customer a message through a notifier. The test must not send a real SMS, so it hands the function a Mock from unittest.mock. A Mock accepts any call, records it, and lets the test ask afterwards. Here the notifier is simply passed in as an argument; how to design classes that receive their helpers this way is the topic of Dependency injection. The repository is the fake from the table above:

confirm_order() کۆی گشتی دەژمێرێت و لە ڕێی notifierـێکەوە پەیامێک بۆ کڕیار دەنێرێت. تاقیکردنەوەکە نابێت ڕاستەقینە بنێرێت، بۆیە Mockـێک لە unittest.mockـەوە دەداتە . Mock هەموو بانگکردنێک وەردەگرێت، تۆماری دەکات و ڕێگە دەدات تاقیکردنەوەکە دواتر بپرسێت. لێرەدا notifier بە سادەیی وەک دەدرێت؛ چۆن دیزاین بکەیت کە یاریدەدەرەکانیان بەم شێوەیە وەربگرن بابەتی تێخستنی وابەستەییـە. کۆگاکە ئەو Fakeـەیە کە لە خشتەکەی سەرەوەدایە:

The code under testئەو کۆدەی تاقی دەکرێتەوەorders.pyPYTHON
"""Bazaar Cart: the total of a whole order."""
from prices import delivery_fee, line_price


def order_total(lines, repo):
    """Goods + delivery for lines such as [("sumac", 350)], checking the stock."""
    goods = 0
    for name, grams in lines:
        if grams > repo.stock_grams(name):
            raise ValueError(f"not enough {name} in stock")
        goods += line_price(repo.price_per_kg(name), grams)
    return goods + delivery_fee(goods)


def confirm_order(customer, lines, repo, notifier):
    """Work out the total and tell the customer by message."""
    total = order_total(lines, repo)
    notifier.send(customer, f"Bazaar Cart: your order of {total:,} IQD is on its way.")
    return total
A fake repository and a mock notifierکۆگایەکی Fake و notifierـێکی Mocktests/test_orders.pyPYTHON
import unittest
from unittest.mock import Mock

from orders import confirm_order, order_total
from tests.fakes import InMemoryProductRepository


class OrderTest(unittest.TestCase):
    def setUp(self):
        self.repo = InMemoryProductRepository(
            {"sumac": (8000, 2000), "walnuts": (18000, 5000)}
        )

    def test_goods_plus_delivery(self):
        self.assertEqual(order_total([("sumac", 350)], self.repo), 6000)

    def test_more_than_the_stock_is_refused(self):
        with self.assertRaises(ValueError):
            order_total([("sumac", 2500)], self.repo)

    def test_confirm_sends_one_message(self):
        notifier = Mock()
        total = confirm_order("customer-17", [("sumac", 350)], self.repo, notifier)
        self.assertEqual(total, 6000)
        notifier.send.assert_called_once_with(
            "customer-17", "Bazaar Cart: your order of 6,000 IQD is on its way."
        )
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 9-12

The fake repository: a dictionary of (price per kg, stock in grams).

کۆگا Fakeـەکە: لە (نرخی کیلۆ، کۆگا بە گرام).

Lineدێڕی 21-27

Act, then ask the mock: was send called exactly once, with these two arguments?

جێبەجێکردن، پاشان پرسیار لە Mockـەکە: ئایا send ڕێک یەک جار بەم دوو بانگ کرا؟

If someone changes the text of the message, the mock tells exactly what was expected and what really happened (the lines inside Python's own library are left out, marked …):

ئەگەر کەسێک دەقی پەیامەکە بگۆڕێت، Mockـەکە ڕێک دەڵێت چی چاوەڕوان دەکرا و چی بەڕاستی ڕوویدا (ئەو دێڕانەی ناو خودی Python لابراون، بە … نیشان کراون):

“is on the way” instead of “is on its way”«is on the way» لەبری «is on its way»BASH
$ python3.13 -m unittest tests.test_orders
F..
======================================================================
FAIL: test_confirm_sends_one_message (tests.test_orders.OrderTest.test_confirm_sends_one_message)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/se/bazaar-cart/tests/test_orders.py", line 25, in test_confirm_sends_one_message
    notifier.send.assert_called_once_with(
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^
        "customer-17", "Bazaar Cart: your order of 6,000 IQD is on its way."
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  …
AssertionError: expected call not found.
Expected: send('customer-17', 'Bazaar Cart: your order of 6,000 IQD is on its way.')
  Actual: send('customer-17', 'Bazaar Cart: your order of 6,000 IQD is on the way.')

----------------------------------------------------------------------
Ran 3 tests in 0.002s

FAILED (failures=1)
A sequence picture: the test creates a Mock notifier and calls confirm_order with it. confirm_order calls notifier.send('customer-17', 'Bazaar Cart: your order of 6,000 IQD is on its way.'). The Mock writes the call in its record instead of sending an SMS. The test then asks assert_called_once_with with the same arguments and gets a tick.وێنەیەکی زنجیرەیی: تاقیکردنەوەکە notifierـێکی Mock دروست دەکات و confirm_order پێی بانگ دەکات. confirm_order بانگی notifier.send('customer-17', 'Bazaar Cart: your order of 6,000 IQD is on its way.') دەکات. Mockـەکە لەبری ناردنی نامەی کورت، بانگکردنەکە لە تۆمارەکەیدا دەنووسێت. پاشان تاقیکردنەوەکە assert_called_once_with بە هەمان ئارگیومێنتەکان دەپرسێت و نیشانەی ڕاست وەردەگرێت.
A mock records calls instead of doing the real work.Mock لەبری کردنی کارە ڕاستەقینەکە بانگکردنەکان تۆمار دەکات.CêIT

Every bug gets a testهەموو باگێک تاقیکردنەوەیەک وەردەگرێت

When a bug is reported, the team's rule is: first write a test that fails because of the bug, then fix the code, then keep the test for ever. MIT's course says the same: add the input that showed the bug to your test suite. That test is now a regression test: if anyone brings the bug back, it fails at once. For Soran's sumac report the test already exists — test_sumac_350_g_is_rounded_up. Against version 1.0.0 it fails; after the real fix it passes:

کاتێک ڕاپۆرت دەکرێت، یاسای تیمەکە ئەمەیە: سەرەتا تاقیکردنەوەیەک بنووسە کە بەهۆی سەرنەکەوێت، پاشان کۆدەکە چاک بکەرەوە، پاشان تاقیکردنەوەکە بۆ هەمیشە بهێڵەوە. خولەکەی MIT هەمان شت دەڵێت: ئەو پیشان دا بخەرە ناو . ئەو تاقیکردنەوەیە ئێستا ـە: ئەگەر هەر کەسێک بگەڕێنێتەوە، یەکسەر سەرناکەوێت. بۆ ڕاپۆرتی سوماقەکەی سۆران تاقیکردنەوەکە پێشتر هەیە — test_sumac_350_g_is_rounded_up. لەسەر ⁦1.0.0⁩ سەرناکەوێت؛ دوای چاکسازییە ڕاستەقینەکە سەردەکەوێت:

The same test, before and after the fix in 1.0.1هەمان تاقیکردنەوە، پێش و دوای چاکسازی لە ⁦1.0.1⁩BASH
$ python3.13 -m unittest tests.test_prices
....F
======================================================================
FAIL: test_sumac_350_g_is_rounded_up (tests.test_prices.LinePriceTest.test_sumac_350_g_is_rounded_up)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "~/se/bazaar-cart/tests/test_prices.py", line 8, in test_sumac_350_g_is_rounded_up
    self.assertEqual(line_price(8000, 350), 3000)
    ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 2750.0 != 3000

----------------------------------------------------------------------
Ran 5 tests in 0.001s

FAILED (failures=1)
$ python3.13 -m unittest tests.test_prices
.....
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK
The real fix in 1.0.1: round up with math.ceilچاکسازیی ڕاستەقینە لە ⁦1.0.1⁩: نزیککردنەوە بەرەو سەرەوە بە ⁦math.ceil⁩PYTHON
def round_up_250(amount):
    """Round an amount up to the next 250 IQD."""
    return math.ceil(amount / 250) * 250
A loop of four steps around a growing pile of tests: a bug report (issue #21: sumac 350 g costs 2,750), write a test that fails (red), fix the code until the test passes (green), keep the test in the suite for ever. Every turn of the loop leaves one more test behind.سووڕێک لە چوار هەنگاو بە دەوری کۆمەڵێک تاقیکردنەوەی گەورەبوودا: ڕاپۆرتی باگ (ئیشوو #21: سوماقی 350 گرام 2,750ـە)، تاقیکردنەوەیەک بنووسە کە سەرنەکەوێت (سوور)، کۆدەکە چاک بکەرەوە تا تاقیکردنەوەکە سەردەکەوێت (سەوز)، تاقیکردنەوەکە بۆ هەمیشە لە کۆمەڵەکەدا بهێڵەوە. هەر سووڕێک تاقیکردنەوەیەکی تر بەجێ دەهێڵێت.
Bug → failing test → fix → the test stays. ← تاقیکردنەوەی سەرنەکەوتوو ← چاکسازی ← تاقیکردنەوەکە دەمێنێتەوە.CêIT

Test first: red, green, refactorسەرەتا تاقیکردنەوە: سوور، سەوز، ڕێکخستنەوە

Kent Beck made this habit into a method in the late 1990s: test-driven development (TDD). Martin Fowler describes the loop: write a list of tests you will need; write one test and watch it fail (red); write just enough code to pass (green); then tidy the code without changing what it does (refactor, link code refactoring); repeat. The Bazaar Cart team builds rule R5 for version 1.1.0 this way: 5 % off goods totals from 100,000 IQD, rounded up to 250.

کێنت بێک لە کۆتایی ساڵانی ⁦1990⁩دا ئەم خووەی کرد بە ڕێگایەک: ⁦(TDD)⁩. مارتن فاولەر سووڕەکە وەسف دەکات: لیستی ئەو تاقیکردنەوانە بنووسە کە پێویستت دەبن؛ یەک تاقیکردنەوە بنووسە و ببینە سەرناکەوێت (سوور)؛ تەنیا ئەوەندە کۆد بنووسە کە سەرکەوتووی بکات (سەوز)؛ پاشان کۆدەکە ڕێک بخەرەوە بێ ئەوەی کارەکەی بگۆڕیت ()؛ دووپاتەی بکەرەوە. تیمی Bazaar Cart یاسای ⁦R5⁩ بۆ ⁦1.1.0⁩ بەم شێوەیە دروست دەکات: ⁦5%⁩ داشکان بۆ کۆی کاڵا لە ⁦100,000⁩ دینارەوە، بەرەو سەرەوە نزیککراوە بۆ ⁦250⁩.

  1. Red: the test comes firstسوور: تاقیکردنەوە یەکەم دێت

    Two tests describe the rule before any code exists. Running them gives an error, because discount does not exist yet.

    دوو تاقیکردنەوە یاساکە وەسف دەکەن پێش ئەوەی هیچ کۆدێک هەبێت. جێبەجێکردنیان هەڵە دەدات، چونکە discount هێشتا بوونی نییە.

    tests/test_discount.pyPYTHON
    import unittest
    
    from prices import discount
    
    
    class DiscountTest(unittest.TestCase):
        def test_no_discount_below_100000(self):
            self.assertEqual(discount(99_750), 0)
    
        def test_5_percent_from_100000(self):
            self.assertEqual(discount(100_000), 5000)
    BASH
    $ python3.13 -m unittest tests.test_discount
    E
    ======================================================================
    ERROR: test_discount (unittest.loader._FailedTest.test_discount)
    ----------------------------------------------------------------------
    ImportError: Failed to import test module: test_discount
    Traceback (most recent call last):
      …
      File "~/se/bazaar-cart/tests/test_discount.py", line 3, in <module>
        from prices import discount
    ImportError: cannot import name 'discount' from 'prices' (~/se/bazaar-cart/prices.py)
    
    
    ----------------------------------------------------------------------
    Ran 1 test in 0.000s
    
    FAILED (errors=1)
  2. Green: just enough codeسەوز: تەنیا ئەوەندە کۆد کە پێویستە

    The simplest code that passes both tests.

    سادەترین کۆد کە هەردوو تاقیکردنەوەکە سەرکەوتوو دەکات.

    prices.pyPYTHON
    def discount(goods_total):
        """5 % off goods totals from 100,000 IQD (rule R5)."""
        if goods_total >= 100_000:
            return goods_total * 5 // 100
        return 0
    BASH
    $ python3.13 -m unittest -v tests.test_discount
    test_5_percent_from_100000 (tests.test_discount.DiscountTest.test_5_percent_from_100000) ... ok
    test_no_discount_below_100000 (tests.test_discount.DiscountTest.test_no_discount_below_100000) ... ok
    
    ----------------------------------------------------------------------
    Ran 2 tests in 0.001s
    
    OK
  3. Red again: the next testدووپاتە سوور: تاقیکردنەوەی دواتر

    104,000 IQD × 5 % = 5,200, which R5 rounds up to 5,250. A third test says so — and fails.

    ⁦104,000⁩ دینار × ⁦5% = 5,200⁩، کە ⁦R5⁩ بەرەو سەرەوە نزیکی دەکاتەوە بۆ ⁦5,250⁩. تاقیکردنەوەی سێیەم ئەمە دەڵێت — و سەرناکەوێت.

    tests/test_discount.pyPYTHON
        def test_discount_is_rounded_up_to_250(self):
            self.assertEqual(discount(104_000), 5250)  # 5 % is 5,200
    BASH
    $ python3.13 -m unittest tests.test_discount
    .F.
    ======================================================================
    FAIL: test_discount_is_rounded_up_to_250 (tests.test_discount.DiscountTest.test_discount_is_rounded_up_to_250)
    ----------------------------------------------------------------------
    Traceback (most recent call last):
      File "~/se/bazaar-cart/tests/test_discount.py", line 14, in test_discount_is_rounded_up_to_250
        self.assertEqual(discount(104_000), 5250)  # 5 % is 5,200
        ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
    AssertionError: 5200 != 5250
    
    ----------------------------------------------------------------------
    Ran 3 tests in 0.001s
    
    FAILED (failures=1)
  4. Green, then refactorسەوز، پاشان ڕێکخستنەوە

    Round with the existing round_up_250(); all three pass. Then give the numbers names (DISCOUNT_FROM, DISCOUNT_PERCENT) and run the whole suite again.

    بە round_up_250()ـی هەبوو نزیکی بکەرەوە؛ هەر سێکیان سەردەکەون. پاشان ناو بدە بە ژمارەکان (DISCOUNT_FROM، DISCOUNT_PERCENT) و هەموو کۆمەڵەکە دووپاتە جێبەجێ بکەرەوە.

    prices.py (1.1.0)PYTHON
    DISCOUNT_FROM = 100_000  # goods total in IQD (rule R5)
    DISCOUNT_PERCENT = 5
    
    
    def discount(goods_total):
        """The discount in IQD: 5 % from 100,000 IQD of goods, rounded up to 250."""
        if goods_total < DISCOUNT_FROM:
            return 0
        return round_up_250(goods_total * DISCOUNT_PERCENT / 100)
    BASH
    $ python3.13 -m unittest
    .................
    ----------------------------------------------------------------------
    Ran 17 tests in 0.001s
    
    OK
Animation of the TDD loop for rule R5: a red light while test_discount fails to import; green when two tests pass; red again when the rounding test fails with 5200 != 5250; green with round_up_250; a refactor step that names the numbers while all 17 tests stay green.
Animation of the TDD loop for rule R5: a red light while test_discount fails to import; green when two tests pass; red again when the rounding test fails with 5200 != 5250; green with round_up_250; a refactor step that names the numbers while all 17 tests stay green.
Red → green → refactor, three times round.سوور ← سەوز ← ڕێکخستنەوە، سێ جار بە دەوریدا.CêIT

Code coverage: what the tests touchداپۆشینی کۆد: تاقیکردنەوەکان دەست لە چی دەدەن

Code coverage measures which lines (and which branches of each if) the tests actually ran, as a percentage. Low coverage shows code that no test has ever touched — a good place to add tests. But 100 % does not mean bug-free. Look at the > bug again, tested with just two orders of 30,000 and 70,000 IQD:

دەپێوێت تاقیکردنەوەکان بەڕاستی کام دێڕەکانیان (و کام ڕێڕەوی هەر ifـێک) جێبەجێ کردووە، بە . داپۆشینی کەم کۆدێک پیشان دەدات کە هیچ تاقیکردنەوەیەک هەرگیز دەستی لێ نەداوە — شوێنێکی باشە بۆ زیادکردنی تاقیکردنەوە. بەڵام ⁦100%⁩ واتای بێ نییە. دووپاتە سەیری > بکە، کە تەنیا بە دوو داواکاریی ⁦30,000⁩ و ⁦70,000⁩ دینار تاقی کراوەتەوە:

Every line and both branches run — and the bug staysهەموو دێڕێک و هەردوو ڕێڕەوەکە جێبەجێ دەبن — و باگەکە دەمێنێتەوەPYTHON
def delivery_fee(goods_total):
    if goods_total > FREE_DELIVERY_FROM:   # bug: should be >=
        return 0
    return DELIVERY_FEE


# tests: delivery_fee(30_000) == 3000 and delivery_fee(70_000) == 0

Both tests pass, every line runs, both branches of the if run: 100 %. Yet an order of exactly 50,000 IQD still pays delivery. Coverage tells you what was run, not whether the right things were checked — only the boundary test 50,000 catches this. The usual Python tool is coverage.py (version 7.17 in November 2026). It is not in the standard library, so the commands below are a labelled example, not run here:

هەردوو تاقیکردنەوەکە سەردەکەون، هەموو دێڕێک جێبەجێ دەبێت، هەردوو ڕێڕەوی if جێبەجێ دەبن: ⁦100%⁩. کەچی داواکارییەکی ڕێک ⁦50,000⁩ دینار هێشتا پارەی گەیاندن دەدات. داپۆشین پێت دەڵێت چی جێبەجێ کرا، نەک ئایا شتە دروستەکان پشکنران — تەنیا تاقیکردنەوەی سنووری ⁦50,000⁩ ئەمە دەگرێت. ئامرازە باوەکەی Python ⁦coverage.py⁩ـە ( ⁦7.17⁩ لە تشرینی دووەمی ⁦2026⁩). لە ستانداردا نییە، بۆیە ئەم خوارەوە نموونەی ناونووسکراون، لێرە جێبەجێ نەکراون:

Labelled example (not run): coverage.pyنموونەی ناونووسکراو (جێبەجێ نەکراوە): ⁦coverage.py⁩BASH
python3.13 -m pip install coverage     # once, inside a virtual environment
coverage run -m unittest                # run the suite while measuring
coverage report -m                      # Stmts, Miss, Cover %, Missing line numbers
coverage html                           # a web page with the missed lines in colour
The delivery_fee code with every line marked green as run, and a badge 100 % coverage. Beside it a number line where 30,000 and 70,000 are tested, but 50,000 — the edge — is not; at 50,000 the code charges 3,000 instead of 0. Caption: run is not the same as checked.کۆدی delivery_fee کە هەموو دێڕێکی بە سەوز وەک جێبەجێکراو نیشان کراوە و نیشانەی 100% داپۆشین. لە تەنیشتیەوە هێڵێکی ژمارە کە 30,000 و 70,000 تاقی کراونەتەوە، بەڵام 50,000 — سنوورەکە — نا؛ لە 50,000دا کۆدەکە 3,000 وەردەگرێت لەبری 0. ڕوونکردنەوە: جێبەجێکردن هەمان پشکنین نییە.
100 % coverage, one bug left.⁦100%⁩ داپۆشین، یەک ماوە.CêIT

Code coverage: what the tests touchداپۆشینی کۆد: تاقیکردنەوەکان دەست لە چی دەدەن

doctest: examples that test themselvesdoctest: نموونەگەلێک کە خۆیان تاقی دەکەنەوە

A third style comes with Python: doctest. You write a short example session inside a function's docstring — >>> and the expected answer — and python3.13 -m doctest runs it. The examples teach the reader and stay correct. With -v it lists every example; without it, a passing file prints nothing at all:

شێوازێکی سێیەم لەگەڵ Python دێت: doctest. دانیشتنێکی کورتی نموونە لە ناو زنجیرەنووسەی بەڵگەنامەـی دەنووسیت — >>> و وەڵامی چاوەڕوانکراو — و python3.13 -m doctest جێبەجێی دەکات. نموونەکان خوێنەر فێر دەکەن و دروست دەمێننەوە. بە -v هەموو نموونەیەک لیست دەکات؛ بەبێ ئەوە، سەرکەوتوو هیچ چاپ ناکات:

Examples inside the docstringنموونەکان لە ناو زنجیرەنووسەی بەڵگەنامەکەداprices.pyPYTHON
def round_up_250(amount):
    """Round an amount up to the next 250 IQD.

    >>> round_up_250(2800)
    3000
    >>> round_up_250(9000)
    9000
    """
    return math.ceil(amount / 250) * 250
Running the examplesجێبەجێکردنی نموونەکانBASH
$ python3.13 -m doctest -v prices.py
Trying:
    round_up_250(2800)
Expecting:
    3000
ok
Trying:
    round_up_250(9000)
Expecting:
    9000
ok
4 items had no tests:
    prices
    prices.delivery_fee
    prices.discount
    prices.line_price
1 item passed all tests:
   2 tests in prices.round_up_250
2 tests in 5 items.
2 passed.
Test passed.
$ python3.13 -m doctest prices.py

pytest: the same tests, shorterpytest: هەمان تاقیکردنەوەکان، کورتتر

Many Python teams use pytest, a free library that is not part of Python (newest version 9.2 in November 2026; install it with pip inside a virtual environment). A test is a plain function whose name starts with test_, and the check is the ordinary assert — pytest rewrites it to explain failures. @pytest.mark.parametrize runs one test with a table of inputs. pytest also runs unittest tests unchanged. These tests were run with pytest 8.4.2, which was already installed on our test computer:

زۆر تیمی Python pytest بەکار دەهێنن، بێبەرانبەر کە بەشێک نییە لە Python (نوێترین ⁦9.2⁩ لە تشرینی دووەمی ⁦2026⁩؛ بە pip لە ناو ژینگەیەکی گریمانەییدا دایبمەزرێنە). تاقیکردنەوەیەک سادەیە کە ناوەکەی بە test_ دەست پێدەکات و پشکنینەکە assertـی ئاساییە — pytest دووپاتە دەینووسێتەوە بۆ ڕوونکردنەوەی سەرنەکەوتنەکان. @pytest.mark.parametrize یەک تاقیکردنەوە بە خشتەیەک لە جێبەجێ دەکات. pytest تاقیکردنەوەکانی unittestـیش بێ گۆڕانکاری جێبەجێ دەکات. ئەم تاقیکردنەوانە بە ⁦pytest 8.4.2⁩ جێبەجێ کران، کە پێشتر لەسەر کۆمپیوتەری تاقیکردنەوەکەمان دامەزرابوو:

The price tests in pytest styleتاقیکردنەوەکانی نرخ بە شێوازی pytesttests/test_pytest_style.pyPYTHON
import pytest

from prices import delivery_fee, line_price


def test_sumac_350_g_is_rounded_up():
    assert line_price(8000, 350) == 3000


def test_exact_price_stays_the_same():
    assert line_price(18000, 500) == 9000


def test_less_than_100_g_is_refused():
    with pytest.raises(ValueError):
        line_price(8000, 99)


@pytest.mark.parametrize("goods_total, fee", [(49_750, 3000), (50_000, 0)])
def test_delivery_fee(goods_total, fee):
    assert delivery_fee(goods_total) == fee
Against Shilan's quick fix: pytest explains the assertلەسەر چاکسازییە خێراکەی شیلان: pytest assertـەکە ڕوون دەکاتەوەBASH
$ pytest -q tests/test_pytest_style.py
.F...                                                                                        [100%]
============================================= FAILURES =============================================
_________________________________ test_exact_price_stays_the_same __________________________________

    def test_exact_price_stays_the_same():
>       assert line_price(18000, 500) == 9000
E       assert 9250.0 == 9000
E        +  where 9250.0 = line_price(18000, 500)

tests/test_pytest_style.py:11: AssertionError
===================================== short test summary info ======================================
FAILED tests/test_pytest_style.py::test_exact_price_stays_the_same - assert 9250.0 == 9000
1 failed, 4 passed in 0.03s
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 8-9

+ where 9250.0 = line_price(18000, 500) shows the value of each part of the failing line.

+ where 9250.0 = line_price(18000, 500) بەهای هەر بەشێکی دێڕە سەرنەکەوتووەکە پیشان دەدات.

The same ideas, two spellings.هەمان بیرۆکەکان، بە دوو شێوەی نووسین.
Featureتایبەتمەندیunittestpytest
Comes with Pythonلەگەڵ Python دێتyesبەڵێno: pip install pytestنەخێر: pip install pytest
A test is…تاقیکردنەوەیەک…a method in a TestCase class لە TestCasea plain test_… function سادەی test_…
The checkپشکنینەکەself.assertEqual(a, b)self.assertEqual(a, b)assert a == bassert a == b
A table of casesخشتەیەک لە حاڵەتەکانwith self.subTest(...)with self.subTest(...)@pytest.mark.parametrize@pytest.mark.parametrize

Continuous integration: tests on every pushپێکەوەبەستنی بەردەوام: تاقیکردنەوە لەسەر هەموو پوشێک

Tests help only if someone runs them. Continuous integration (CI) makes sure they always run: everyone merges small changes into main often — Martin Fowler says at least once a day — and a server builds and tests every change automatically, so a broken change is found within minutes and fixed first. The fixed list of automatic steps is a pipeline: get the code → set up Python → run all tests → report ✓ or ✗. On GitHub the pipeline runs with GitHub Actions on a runner, a fresh virtual machine for every run. Its ✓ or ✗ appears on the pull request, and a protected main can refuse to merge until it is green.

تاقیکردنەوەکان تەنیا ئەو کاتە یارمەتی دەدەن کە کەسێک جێبەجێیان بکات. ⁦(CI)⁩ دڵنیا دەبێتەوە کە هەمیشە جێبەجێ دەکرێن: هەمووان گۆڕانکاریی بچووک زوو زوو لەگەڵ main بەیەک دەکەن — مارتن فاولەر دەڵێت لانیکەم ڕۆژی یەک جار — و هەموو گۆڕانکارییەک خۆکارانە دەکات و تاقی دەکاتەوە، بۆیە گۆڕانکارییەکی شکاو لە چەند خولەکێکدا دەدۆزرێتەوە و پێش هەموو کارێکی تر چاک دەکرێتەوە. لیستە جێگیرەکەی هەنگاوە خۆکارەکان ـە: هێنانی کۆد ← ئامادەکردنی Python ← جێبەجێکردنی هەموو تاقیکردنەوەکان ← ڕاپۆرتی ✓ یان ✗. لە GitHub ئەم زنجیرەیە بە GitHub Actions لەسەر runnerـێک جێبەجێ دەبێت، کە نوێیە بۆ هەر جێبەجێکردنێک. ✓ یان ✗ـەکەی لەسەر دەردەکەوێت و mainـی پارێزراو دەتوانێت ڕێگە بە نەدات تا سەوز دەبێت.

A left-to-right pipeline: Shilan pushes a branch or opens a pull request; GitHub Actions starts a fresh Ubuntu runner; step 1 checks out the code, step 2 sets up Python 3.13, step 3 runs python -m unittest -v; the result goes back to the pull request as a green tick (all 17 tests passed, merge allowed) or a red cross (a test failed, merge blocked until fixed).زنجیرەیەک لە چەپەوە بۆ ڕاست: شیلان لقێک پوش دەکات یان داواکارییەکی بەیەککردن دەکاتەوە؛ GitHub Actions runnerـێکی نوێی Ubuntu دەست پێدەکات؛ هەنگاوی 1 کۆدەکە دەهێنێت، هەنگاوی 2 Python 3.13 ئامادە دەکات، هەنگاوی 3 python -m unittest -v جێبەجێ دەکات؛ ئەنجامەکە دەگەڕێتەوە بۆ داواکاریی بەیەککردنەکە وەک نیشانەیەکی سەوز (هەر 17 تاقیکردنەوەکە سەرکەوتن، بەیەککردن ڕێگەپێدراوە) یان خاچێکی سوور (تاقیکردنەوەیەک سەرنەکەوت، بەیەککردن ڕێگری لێ دەکرێت تا چاک دەکرێتەوە).
Every push and pull request runs the whole suite.هەموو و هەموو هەموو کۆمەڵەکە جێبەجێ دەکات.CêIT

Continuous integration: tests on every pushپێکەوەبەستنی بەردەوام: تاقیکردنەوە لەسەر هەموو پوشێک

A GitHub Actions workflowworkflowـێکی GitHub Actions

A workflow is a YAML file in .github/workflows/. This one is a labelled example, not run here (it needs a GitHub repository); the action versions are the ones GitHub's Python guide shows in November 2026 — check the guide when you copy it. It runs on every push to main and on every pull request:

workflow YAMLـە لە .github/workflows/. ئەمەیان نموونەیەکی ناونووسکراوە، لێرە جێبەجێ نەکراوە (پێویستی بە GitHub هەیە)؛ actionـەکان ئەوانەن کە ڕێبەری Pythonـی GitHub لە تشرینی دووەمی ⁦2026⁩ـدا پیشانیان دەدات — کاتێک کۆپیی دەکەیت سەیری ڕێبەرەکە بکە. لەسەر هەموو بۆ main و هەموو جێبەجێ دەبێت:

Labelled example (not run): CI for Bazaar Cartنموونەی ناونووسکراو (جێبەجێ نەکراوە): CI بۆ Bazaar Cart.github/workflows/tests.ymlYAML
name: Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-python@v5
        with:
          python-version: "3.13"
      - name: Run the unit tests
        run: python -m unittest -v
Explain the codeڕوونکردنەوەی کۆدەکە
Lineدێڕی 3-6

The events: a push to main, or any pull request.

ڕووداوەکان: بۆ main، یان هەر .

Lineدێڕی 10

A fresh Ubuntu Linux runner for each run.

runnerـێکی نوێی Ubuntu Linux بۆ هەر جێبەجێکردنێک.

Lineدێڕی 12-15

Ready-made actions: get the repository's code, then install Python 3.13.

actionـی ئامادە: کۆدی بهێنە، پاشان ⁦Python 3.13⁩ دابمەزرێنە.

Lineدێڕی 16-17

The same command you run at home. If any test fails, the step fails and the PR shows ✗.

هەمان کە لە ماڵەوە جێبەجێی دەکەیت. ئەگەر هەر تاقیکردنەوەیەک سەرنەکەوێت، هەنگاوەکە سەرناکەوێت و PRـەکە ✗ پیشان دەدات.

GitHub runs standard workflows free for public repositories; private repositories get a monthly allowance (as of November 2026). With a protected main that requires this check, Karwan's > mistake from earlier could never reach the shop.

GitHub workflowـە ستانداردەکان بۆ گشتی بێبەرانبەر جێبەجێ دەکات؛ تایبەت بڕێکی مانگانە وەردەگرن (تا تشرینی دووەمی ⁦2026⁩). بە mainـێکی پارێزراو کە ئەم پشکنینە پێویست دەکات، هەڵە >ـەکەی پێشووی کاروان هەرگیز نەیدەتوانی بگاتە دوکانەکە.

Two schematic merge boxes of a pull request. Left, after the first push: a red cross, 'Tests / test — failing', and a grey, disabled Merge button with 'Required check failed'. Right, after Karwan pushes a fix: a green tick, 'Tests / test — successful', and a green Merge pull request button.دوو سندوقی سادەی Merge لە داواکارییەکی بەیەککردندا. چەپ، دوای یەکەم پوش: خاچێکی سوور، «Tests / test — failing» و دوگمەیەکی خۆڵەمێشیی ناچالاکی Merge بە «Required check failed». ڕاست، دوای ئەوەی کاروان چاکسازییەک پوش دەکات: نیشانەیەکی سەوز، «Tests / test — successful» و دوگمەیەکی سەوزی Merge pull request.
The check on the pull request: red blocks, green allows.پشکنینەکە لەسەر : سوور ڕێگری دەکات، سەوز ڕێگە دەدات.CêIT

Tests in Java, Dart and PHPتاقیکردنەوە لە Java، Dart و PHP

Every language on CêIT has the same ideas — test cases, assertions, a runner, fixtures — with different names. The lines below are labelled examples checked against each tool's documentation in November 2026, not run here:

هەموو زمانێکی CêIT هەمان بیرۆکەکانی هەیە — ، ، جێبەجێکەرێک، — بە ناوی جیاواز. ئەم دێڕانەی خوارەوە نموونەی ناونووسکراون کە لە تشرینی دووەمی ⁦2026⁩ـدا لەگەڵ بەڵگەنامەی هەر ئامرازێک بەراورد کراون، لێرە جێبەجێ نەکراون:

Same idea, four languages (labelled examples).هەمان بیرۆکە، چوار زمان (نموونەی ناونووسکراو).
LanguageزمانToolئامرازOne testیەک تاقیکردنەوەRunجێبەجێکردن
Pythonunittestself.assertEqual(line_price(8000, 350), 3000)python3.13 -m unittest
JavaJUnit 6 (Java 17+)@Test void sumac() { assertEquals(3000, Prices.linePrice(8000, 350)); }Gradle or MavenGradle یان Maven
Dart / Flutterpackage:test / flutter_testtest('sumac', () { expect(linePrice(8000, 350), 3000); });dart test / flutter test
PHPPHPUnit$this->assertSame(3000, linePrice(8000, 350));vendor/bin/phpunit

Versions to know (November 2026): JUnit 6 is the current JUnit and needs Java 17 or newer; PHPUnit 13 is the newest PHPUnit but needs a newer PHP than XAMPP's 8.2 — with PHP 8.2 use PHPUnit 11. Flutter's own example of testing a view model with fakes is in Testing a view model with fakes.

کە پێویستە بیانزانیت (تشرینی دووەمی ⁦2026): JUnit 6 JUnit⁩ـی ئێستایە و ⁦Java 17⁩ یان نوێتری دەوێت؛ ⁦PHPUnit 13⁩ نوێترین PHPUnitـە بەڵام PHPـێکی نوێتر لە ⁦8.2⁩ـی XAMPP دەوێت — لەگەڵ ⁦PHP 8.2 PHPUnit 11⁩ بەکار بهێنە. نموونەی خودی Flutter بۆ تاقیکردنەوەی بە Fake لە تاقیکردنەوەی مۆدێلی دیمەن بە Fakeدایە.

Common mistakesهەڵە باوەکان

Each of these was run with Python 3.13; the messages are real.

هەر یەکێک لەمانە بە ⁦Python 3.13⁩ جێبەجێ کراوە؛ پەیامەکان ڕاستەقینەن.

  • Tests that depend on each other or on order: each test must make its own data (use setUp).
  • Tests that depend on today's date, the clock, the internet or a real SMS service: they fail at random (flaky). Pass in fakes.
  • Testing private details (a helper's exact steps) instead of what the code promises: every tidy-up breaks the tests. Test behaviour.
  • تاقیکردنەوەی پشتبەستوو بە یەکتر یان بە ڕیزبەندی: هەر تاقیکردنەوەیەک دەبێت خۆی دروست بکات (setUp بەکار بهێنە).
  • تاقیکردنەوەی پشتبەستوو بە ڕێکەوتی ئەمڕۆ، کاتژمێر، یان خزمەتگوزارییەکی ڕاستەقینەی : بە هەڕەمەکی سەرناکەون (flaky). Fake بدە.
  • تاقیکردنەوەی وردەکاریی ناوەکی (هەنگاوە وردەکانی یاریدەدەرێک) لەبری ئەوەی کۆدەکە بەڵێنی دەدات: هەر ڕێکخستنەوەیەک تاقیکردنەوەکان دەشکێنێت. ڕەفتار تاقی بکەرەوە.

Exercisesڕاهێنانەکان

ExerciseڕاهێنانBoundary tests for R2تاقیکردنەوەی سنوور بۆ ⁦R2⁩

Rule R2: each line must be 100 g to 10,000 g. List the weights you would test and write them as one unittest test with subTest.

یاسای ⁦R2⁩: هەر دێڕێک دەبێت ⁦100⁩ تا ⁦10,000⁩ گرام بێت. ئەو کێشانە لیست بکە کە تاقیان دەکەیتەوە و وەک یەک تاقیکردنەوەی unittest بە subTest بیاننووسە.

Show answerوەڵامەکە پیشان بدە

Partitions: too light, allowed, too heavy. Edges: 99 and 100, 10,000 and 10,001 (or 10,050). Add 0 and a negative weight as edge cases. The suite in Which inputs? does this; here is a compact version:

بەشەکان: زۆر سووک، ڕێگەپێدراو، زۆر قورس. سنوورەکان: ⁦99⁩ و ⁦100⁩، ⁦10,000⁩ و ⁦10,001⁩ (یان ⁦10,050). 0⁩ و کێشێکی نێگەتیڤ وەک زیاد بکە. کۆمەڵەکەی کام نێوخستەکان؟ ئەمە دەکات؛ ئەمە وەشانێکی کورتە:

PYTHON
    def test_r2_limits(self):
        for grams in [-200, 0, 99, 10_001, 10_050]:
            with self.subTest(grams=grams):
                with self.assertRaises(ValueError):
                    line_price(8000, grams)
        for grams in [100, 10_000]:
            with self.subTest(grams=grams):
                self.assertGreater(line_price(8000, grams), 0)
ExerciseڕاهێنانRead the failureسەرنەکەوتنەکە بخوێنەوە

A run prints AssertionError: 2750.0 != 3000 for test_sumac_350_g_is_rounded_up. Which number did the code return, which did the test expect, and what kind of bug is it?

جێبەجێکردنێک بۆ test_sumac_350_g_is_rounded_up AssertionError: 2750.0 != 3000 چاپ دەکات. کۆدەکە کام ژمارەی گەڕاندەوە، تاقیکردنەوەکە چاوەڕێی کامی دەکرد و چ جۆرە ؟

Show answerوەڵامەکە پیشان بدە

The code returned 2,750 (first value), the test expected 3,000. 8,000 × 0.35 = 2,800 was rounded down to 2,750 instead of up: the 1.0.0 rounding bug (amount // 250 * 250). The .0 also shows a float came back.

کۆدەکە ⁦2,750⁩ـی گەڕاندەوە (بەهای یەکەم)، تاقیکردنەوەکە چاوەڕێی ⁦3,000⁩ـی دەکرد. ⁦8,000 × 0.35 = 2,800⁩ بەرەو خوارەوە نزیک کرایەوە بۆ ⁦2,750⁩ لەبری سەرەوە: نزیککردنەوەی ⁦1.0.0⁩ (amount // 250 * 250). .0ـەکەش پیشان دەدات گەڕاوەتەوە.

ExerciseڕاهێنانA spy by handSpyـێک بە دەست

Write a class FakeNotifier with a send(customer, text) method that stores each message in a list sent. Use it instead of Mock to test confirm_order().

FakeNotifier بنووسە بە send(customer, text) کە هەر پەیامێک لە لیستێکی sentـدا هەڵدەگرێت. لەبری Mock بەکاری بهێنە بۆ تاقیکردنەوەی confirm_order().

Show answerوەڵامەکە پیشان بدە
PYTHON
class FakeNotifier:
    def __init__(self):
        self.sent = []

    def send(self, customer, text):
        self.sent.append((customer, text))


# in OrderTest:
    def test_confirm_with_a_spy(self):
        notifier = FakeNotifier()
        confirm_order("customer-17", [("sumac", 350)], self.repo, notifier)
        self.assertEqual(notifier.sent, [
            ("customer-17", "Bazaar Cart: your order of 6,000 IQD is on its way.")])

This hand-written double is a spy: it works and records its calls. Mocks save typing; hand-written fakes are easier to read. The dependency injection lesson uses the same idea.

ئەم جێگرەوە دەستنووسە Spyـە: کار دەکات و بانگکردنەکانی تۆمار دەکات. Mock کاتی نووسین دەپارێزێت؛ Fakeـی دەستنووس ئاسانتر دەخوێنرێتەوە. وانەی تێخستنی وابەستەیی هەمان بیرۆکە بەکار دەهێنێت.

ExerciseڕاهێنانTDD the delivery rule for ErbilTDD بۆ یاسای گەیاندنی هەولێر

Soran wants to deliver to Erbil too: 7,000 IQD, free from 100,000 IQD of goods. Using red → green → refactor, write the tests first for delivery_fee(goods_total, city) and then the code. Which values do you test?

سۆران دەیەوێت بۆ هەولێریش بگەیەنێت: ⁦7,000⁩ دینار، لە ⁦100,000⁩ دیناری کاڵاوە بێبەرانبەر. بە سوور ← سەوز ← ڕێکخستنەوە، سەرەتا تاقیکردنەوەکان بۆ delivery_fee(goods_total, city) بنووسە و پاشان کۆدەکە. کام بەهایانە تاقی دەکەیتەوە؟

Show answerوەڵامەکە پیشان بدە

Tests first: Sulaimani 49,750 → 3,000 and 50,000 → 0 (the old rule must not change — regression); Erbil 99,750 → 7,000, 100,000 → 0, 100,250 → 0; and an unknown city → ValueError. Then a small table of cities in the code, refactored once all tests are green.

سەرەتا تاقیکردنەوەکان: سلێمانی ⁦49,750⁩ ← ⁦3,000⁩ و ⁦50,000⁩ ← 0 (یاسا کۆنەکە نابێت بگۆڕێت — پاشەکشە)؛ هەولێر ⁦99,750⁩ ← ⁦7,000⁩، ⁦100,000⁩ ← 0، ⁦100,250⁩ ← 0؛ و شارێکی نەناسراو ← ValueError. پاشان خشتەیەکی بچووکی شارەکان لە کۆدەکەدا، کە دوای سەوزبوونی هەموو تاقیکردنەوەکان ڕێک دەخرێتەوە.

ExerciseڕاهێنانPlace them on the pyramidلەسەر هەرەمەکە دایانبنێ

Unit, integration or end-to-end? (a) round_up_250(2800) == 3000; (b) order_total() with the real SQLite repository; (c) a script that starts the app, types an order and reads the receipt on screen; (d) confirm_order() with a fake repository and a Mock notifier.

یەکە، پێکەوەبەستن یان سەرتاسەری؟ ⁦(a)⁩ round_up_250(2800) == 3000؛ ⁦(b)⁩ order_total() لەگەڵ کۆگای ڕاستەقینەی ؛ ⁦(c)⁩ سکریپتێک کە ئەپەکە دەکاتەوە، داواکارییەک دەنووسێت و پسوولەکە لەسەر شاشە دەخوێنێتەوە؛ ⁦(d)⁩ confirm_order() لەگەڵ کۆگایەکی Fake و notifierـێکی Mock.

Show answerوەڵامەکە پیشان بدە

(a) unit; (b) integration; (c) end-to-end; (d) unit — its collaborators are test doubles, so only confirm_order() and order_total() logic is really tested.

⁦(a)⁩ یەکە؛ ⁦(b)⁩ پێکەوەبەستن؛ ⁦(c)⁩ سەرتاسەری؛ ⁦(d)⁩ یەکە — هاوکارەکانی جێگرەوەی تاقیکردنەوەن، بۆیە تەنیا لۆجیکی confirm_order() و order_total() بەڕاستی تاقی دەکرێتەوە.

Summaryپوختە

  • An automated test calls real code with chosen input and checks the answer: arrange, act, assert. A regression is a bug in something that used to work; running all tests after every change catches it.
  • unittest: TestCase classes, test_… methods (test cases), assertEqual/assertRaises/assertAlmostEqual, setUp fixtures, python3.13 -m unittest discovers tests/test*.py. Read ., F, E and the a != b line.
  • Choose inputs by partitions plus edge cases (49,750 / 50,000; 99 / 100 g). The test pyramid: many unit tests, some integration tests (SQLite in memory), few end-to-end tests.
  • Test doubles — dummy, stub, fake, spy, mock — replace slow or risky parts; unittest.mock.Mock records calls (assert_called_once_with).
  • Every bug gets a failing test first (regression test). TDD: red → green → refactor. Coverage shows what ran, not what was checked: 100 % ≠ bug-free. doctest and pytest are two more styles.
  • Continuous integration: merge small changes often and let a pipeline (GitHub Actions) run every test on every push and pull request; a protected main accepts only green PRs.
  • کۆدی ڕاستەقینە بە هەڵبژێردراو بانگ دەکات و وەڵامەکە دەپشکنێت: ئامادەکردن، جێبەجێکردن، دڵنیابوونەوە. پاشەکشە لە شتێکدا کە پێشتر کاری دەکرد؛ جێبەجێکردنی هەموو تاقیکردنەوەکان دوای هەر گۆڕانکارییەک دەیگرێت.
  • unittest: TestCase، test_… ()، assertEqual/assertRaises/assertAlmostEqual، setUp، python3.13 -m unittest tests/test*.py دەدۆزێتەوە. .، F، E و دێڕی a != b بخوێنەوە.
  • بە بەشەکان و هەڵبژێرە (⁦49,750 / 50,000⁩؛ ⁦99 / 100⁩ گرام). : زۆر ، هەندێک ( لە )، کەمێک تاقیکردنەوەی سەرتاسەری.
  • جێگرەوەکانی تاقیکردنەوە — dummy، stub، fake، spy، mock — لەبری بەشە خاو یان مەترسیدارەکان دادەنرێن؛ unittest.mock.Mock بانگکردنەکان تۆمار دەکات (assert_called_once_with).
  • هەموو سەرەتا تاقیکردنەوەیەکی سەرنەکەوتوو وەردەگرێت (). TDD: سوور ← سەوز ← ڕێکخستنەوە. داپۆشین پیشان دەدات چی جێبەجێ بوو، نەک چی پشکنرا: ⁦100%⁩ ≠ بێ. doctest و pytest دوو شێوازی ترن.
  • : گۆڕانکاریی بچووک زوو زوو بەیەک بکە و بهێڵە ⁦(GitHub Actions)⁩ هەموو تاقیکردنەوەیەک لەسەر هەموو و هەموو جێبەجێ بکات؛ mainـی پارێزراو تەنیا PRـی سەوز وەردەگرێت.

Next: put each rule where it belongs, so it can be tested on its own — Layers and business logic.

داهاتوو: هەر یاسایەک بخە شوێنی خۆی، بۆ ئەوەی بە تەنیا تاقی بکرێتەوە — چینەکان و لۆجیکی کاروبار.

Lesson complete!وانەکە تەواو بوو!

Well done. Review the key terms, then go on to the next lesson.دەستخۆش. سەیرێکی زاراوە سەرەکییەکان بکەرەوە، پاشان بڕۆ بۆ وانەی داهاتوو.

Key termsزاراوە سەرەکییەکان

Next lessonوانەی داهاتوو

Layers and Business Logic: Where Each Rule Livesچینەکان و لۆجیکی کاروبار: هەر یاسایەک لە کوێ دەژی

References and further readingسەرچاوەکان و خوێندنەوەی زیاتر

This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.

Appearanceڕووکار

Modeدۆخ
Styleشێواز
Coloursڕەنگەکان

Saved on this device only.تەنیا لەسەر ئەم ئامێرە پاشەکەوت دەکرێت.