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 و هەڵە باوەکان — هەموو بە دەرخستەی ڕاستەقینە.
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 دەبینێت و دڵخۆشە:
def round_up_250(amount):
"""Round an amount up to the next 250 IQD."""
return amount // 250 * 250 + 250 # Shilan's quick fix$ python3.13 -c "from prices import line_price; print(line_price(8000, 350)); print(line_price(18000, 500))"
3000.0
9250.0Sumac 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. چاکسازییەکە شتێکی تێکدا کە پێشتر کاری دەکرد: پاشەکشەیەک. کەس گوێزی تاقی نەکردەوە، چونکە پشکنینی هەموو حاڵەتێک بە دەست دوای هەموو گۆڕانکارییەک خاو و بێزارکەرە. چارەسەرەکە کۆدێکە کە کۆد دەپشکنێت: . چوار ئاستەکەی لە بەرنامەسازی چییە؟ـدان؛ ئەم وانەیە پیشان دەدات خودی تاقیکردنەوەکان چۆن دەنووسرێن و جێبەجێ دەکرێن.
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 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.")Traceback (most recent call last):
File "~/se/bazaar-cart/check_prices.py", line 12, in <module>
assert line_price(18000, 500) == 9000, "walnuts 500 g must stay 9,000"
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: walnuts 500 g must stay 9,000Explain 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
AssertionErrorand 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. چاپ دەکات — لە کەمتر لە چرکەیەکدا، هەر چەند جار بتەوێت.
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… . هەموو پێکەوە پێکدەهێنن.
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
assertRaisespasses only if the code inside raisesValueError(rule R2: at least 100 g).assertRaisesتەنیا ئەو کاتە سەردەکەوێت کە کۆدی ناوەوەValueErrorبەرز بکاتەوە (یاسای R2: لانیکەم 100 گرام).
$ 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
OKReading 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 هەڵە)، پاشان بۆ هەر کێشەیەک بلۆکێک، پاشان پوختەکە:
$ 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)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 خشتەیەکی تەواو لە حاڵەتەکان لە یەک تاقیکردنەوەدا جێبەجێ دەکات و ناوی ئەو حاڵەتە دەبات کە سەرناکەوێت:
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ـی نووسیبوو. ئەم کۆمەڵەیە یەکسەر دەیگرێت و تەنانەت دەڵێت کام ڕیز:
> 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)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() دوای ئەو، بۆ ئەوەی هەموو تاقیکردنەوەیەک پاک دەست پێبکات و هیچ تاقیکردنەوەیەک پشت بە یەکێکی تر نەبەستێت.
$ 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
OKThe 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.
تاقیکردنەوەکان قەبارەیان هەیە. بەشێکی بچووک بە تەنیا دەپشکنێت و لە میلیچرکەیەکدا جێبەجێ دەبێت. بەشەکان پێکەوە دەپشکنێت — کۆدەکەمان لەگەڵ ڕاستەقینە. تاقیکردنەوەی سەرتاسەری (تاقیکردنەوەی سیستەم) هەموو ئەپەکە وەک بەکار دەهێنێت و خاو و ناسکە. ـی مایک کۆن دەڵێت: لە بنەوە زۆر بنووسە، لە ناوەڕاست هەندێک و لە سەرەوە تەنیا کەمێک تاقیکردنەوەی سەرتاسەری. ماڵپەڕەکەی مارتن فاولەر ئاگاداری قوچەکی بەستەنی پێچەوانە دەکاتەوە — زۆربەی تاقیکردنەوەکان سەرتاسەری و خاو — و زیاد دەکات: ئەگەر تاقیکردنەوەیەکی ئاستبەرز دۆزییەوە کە هیچ نەیگرت، ئەو کەمە زیاد بکە.
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()ـدا نوێ دروست دەکرێت — هیچ بەجێ ناهێڵێت:
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ـی ڕاستەقینەوە خوێندراوەتەوە.
$ 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
OKTest 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:
هەندێک جار بەشە ڕاستەقینەکە خاوە، پارەی تێدەچێت یان کارێک دەکات کە ناتوانیت بیگەڕێنیتەوە — وەک ناردنی بۆ کڕیارێکی ڕاستەقینە. لە تاقیکردنەوەدا جێگرەوەیەکی لەبری دادەنێیت، وەک ئەوەی لە فیلمدا کەسێکی تر لە دیمەنە مەترسیدارەکاندا لەبری ئەکتەرەکە یاری دەکات. جێرارد مێزارۆس ئەم جێگرەوانەی ناو نا جێگرەوەکانی تاقیکردنەوە ( بە مانا فراوانەکەی) و ماڵپەڕەکەی مارتن فاولەر پێنج جۆریان دەژمێرێت:
| Kindجۆر | What it doesچی دەکات | Bazaar Cart exampleنموونەی Bazaar Cart |
|---|---|---|
| Dummy | is passed in but never usedدەدرێت بەڵام هەرگیز بەکار نایەت | a notifier for a test that sends nothingnotifierـێک بۆ تاقیکردنەوەیەک کە هیچ نانێرێت |
| Stub | returns fixed answersوەڵامی جێگیر دەداتەوە | price_per_kg() always returns 8000price_per_kg() هەمیشە 8000 دەگەڕێنێتەوە |
| Fake | really works, but takes a shortcutبەڕاستی کار دەکات، بەڵام ڕێگایەکی کورت دەگرێت | InMemoryProductRepository: a dictionary instead of a databaseInMemoryProductRepository: لەبری |
| Spy | a stub that also records its callsstubـێک کە بانگکردنەکانیشی تۆمار دەکات | a notifier that keeps a list of sent messagesnotifierـێک کە لیستی پەیامە نێردراوەکان هەڵدەگرێت |
| Mock | is told which calls to expect and is checked at the endپێی دەوترێت چاوەڕێی چ بانگکردنێک بکات و لە کۆتاییدا دەپشکنرێت | “send must be called once with this text”«send دەبێت یەک جار بەم دەقە بانگ بکرێت» |
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ـەیە کە لە خشتەکەی سەرەوەدایە:
"""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 totalimport 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
sendcalled 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 لابراون، بە … نیشان کراون):
$ 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)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 سەرناکەوێت؛ دوای چاکسازییە ڕاستەقینەکە سەردەکەوێت:
$ 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
OKdef round_up_250(amount):
"""Round an amount up to the next 250 IQD."""
return math.ceil(amount / 250) * 250Test 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.
- Red: the test comes firstسوور: تاقیکردنەوە یەکەم دێت
Two tests describe the rule before any code exists. Running them gives an error, because
discountdoes 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) - 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 0BASH $ 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 - 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,200BASH $ 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) - 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


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 دینار تاقی کراوەتەوە:
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) == 0Both 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). لە ستانداردا نییە، بۆیە ئەم خوارەوە نموونەی ناونووسکراون، لێرە جێبەجێ نەکراون:
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 colourCode 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 هەموو نموونەیەک لیست دەکات؛ بەبێ ئەوە، سەرکەوتوو هیچ چاپ ناکات:
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$ 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.pypytest: 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 جێبەجێ کران، کە پێشتر لەسەر کۆمپیوتەری تاقیکردنەوەکەمان دامەزرابوو:
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$ 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.03sExplain 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)بەهای هەر بەشێکی دێڕە سەرنەکەوتووەکە پیشان دەدات.
| Featureتایبەتمەندی | unittest | pytest |
|---|---|---|
| Comes with Pythonلەگەڵ Python دێت | yesبەڵێ | no: pip install pytestنەخێر: pip install pytest |
| A test is…تاقیکردنەوەیەک… | a method in a TestCase class لە TestCase | a 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ـی پارێزراو دەتوانێت ڕێگە بە نەدات تا سەوز دەبێت.
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 و هەموو جێبەجێ دەبێت:
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 -vExplain 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ـێکی پارێزراو کە ئەم پشکنینە پێویست دەکات، هەڵە >ـەکەی پێشووی کاروان هەرگیز نەیدەتوانی بگاتە دوکانەکە.
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ـدا لەگەڵ بەڵگەنامەی هەر ئامرازێک بەراورد کراون، لێرە جێبەجێ نەکراون:
| Languageزمان | Toolئامراز | One testیەک تاقیکردنەوە | Runجێبەجێکردن |
|---|---|---|---|
| Python | unittest | self.assertEqual(line_price(8000, 350), 3000) | python3.13 -m unittest |
| Java | JUnit 6 (Java 17+) | @Test void sumac() { assertEquals(3000, Prices.linePrice(8000, 350)); } | Gradle or MavenGradle یان Maven |
| Dart / Flutter | package:test / flutter_test | test('sumac', () { expect(linePrice(8000, 350), 3000); }); | dart test / flutter test |
| PHP | PHPUnit | $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ڕاهێنانەکان
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 و کێشێکی نێگەتیڤ وەک زیاد بکە. کۆمەڵەکەی کام نێوخستەکان؟ ئەمە دەکات؛ ئەمە وەشانێکی کورتە:
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)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ـەکەش پیشان دەدات گەڕاوەتەوە.
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وەڵامەکە پیشان بدە
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ـی دەستنووس ئاسانتر دەخوێنرێتەوە. وانەی تێخستنی وابەستەیی هەمان بیرۆکە بەکار دەهێنێت.
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. پاشان خشتەیەکی بچووکی شارەکان لە کۆدەکەدا، کە دوای سەوزبوونی هەموو تاقیکردنەوەکان ڕێک دەخرێتەوە.
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:
TestCaseclasses,test_…methods (test cases),assertEqual/assertRaises/assertAlmostEqual,setUpfixtures,python3.13 -m unittestdiscoverstests/test*.py. Read.,F,Eand thea != bline. - 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.Mockrecords 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.
doctestand 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
mainaccepts only green PRs.
- کۆدی ڕاستەقینە بە هەڵبژێردراو بانگ دەکات و وەڵامەکە دەپشکنێت: ئامادەکردن، جێبەجێکردن، دڵنیابوونەوە. پاشەکشە لە شتێکدا کە پێشتر کاری دەکرد؛ جێبەجێکردنی هەموو تاقیکردنەوەکان دوای هەر گۆڕانکارییەک دەیگرێت.
- unittest:
TestCase،test_…()،assertEqual/assertRaises/assertAlmostEqual،setUp،python3.13 -m unittesttests/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زاراوە سەرەکییەکان
Layers and Business Logic: Where Each Rule Livesچینەکان و لۆجیکی کاروبار: هەر یاسایەک لە کوێ دەژی
Related lecturesوانە پەیوەندیدارەکان
- GitHub and Teamwork: Remotes, Pull Requests and IssuesGitHub و کاری تیمی: کۆگاکانی کۆدی دوور، داواکارییەکانی بەیەککردن و ئیشووەکان
- Layers and Business Logic: Where Each Rule Livesچینەکان و لۆجیکی کاروبار: هەر یاسایەک لە کوێ دەژی
- Dependency Injection: Handing Objects What They Needتێخستنی وابەستەیی: دانی ئەوەی ئۆبجێکتەکان پێویستیانە
- Errors and Debuggingهەڵەکان و کرمەکوژی
- Operators and Expressionsکارپێکەرەکان و دەربڕینەکان
- SQLite Basicsبنەماکانی SQLite
- Setting Up Flutter and Your First Projectئامادەکردنی Flutter و یەکەم پڕۆژەکەت
- MVVM in Flutter with ProviderMVVM لە Flutterـدا بە Provider
- Modules and importمۆدیوڵەکان و import
References and further readingسەرچاوەکان و خوێندنەوەی زیاتر
- Python 3.13 documentation: unittest — Unit testing framework
- Python 3.13 documentation: unittest.mock — mock object library
- Python 3.13 documentation: doctest
- What's New in Python 3.12 (removed unittest TestCase aliases such as assertEquals)
- Ham Vocke, The Practical Test Pyramid (martinfowler.com, 2018)
- Martin Fowler, TestDouble (2006)
- Martin Fowler, Test Driven Development (updated 2023)
- Martin Fowler, Continuous Integration (updated 2024)
- MIT 6.031 Software Construction: Testing (partitions, boundaries, coverage, regression tests)
- GitHub Docs: Understanding GitHub Actions
- GitHub Docs: Building and testing Python (actions/checkout@v6, actions/setup-python@v5)
- GitHub Docs: GitHub Actions billing (standard runners free for public repositories)
- GitHub Docs: About status checks
- pytest documentation: Get started
- coverage.py documentation
- JUnit User Guide: Writing tests (JUnit 6)
- PHPUnit: Supported versions
- Flutter documentation: Testing Flutter apps
- pub.dev: package:test
This lecture is a first edition. The Kurdish text and terminology are waiting for review by Kurdish IT teachers.ئەم وانەیە چاپی یەکەمە. دەقە کوردییەکە و زاراوەکانی چاوەڕێی پێداچوونەوەی مامۆستایانی کوردی بواری IT ن.