Skip to content

ይህ ትምህርት ገና ወደ አማርኛ አልተተረጎመም፤ ስለዚህ በእንግሊዝኛ ቀርቧል። የእንግሊዝኛውን ገጽ ክፈቱ

Project: Test Birr Ledger

12 min readፕሮጀክት

Birr Ledger v1 works when you try it by hand. This project finds out whether it works when you don't: you'll write a test suite for its functions, find a real bug in v1 with it, and check the suite against planted bugs. Every milestone runs in the browser; the last section runs the same tests on your computer.

The plan

ledger.py is lesson 8.9's finished program, in a tab next to your tests. The tests go in test_ledger.py and use the fixed names:

  • add_expense(expenses, amount, category, note, date=None)
  • total_by_category(expenses)
  • save_expenses(expenses, path=LEDGER_FILE) and load_expenses(path=LEDGER_FILE)
  • a fixture, sample_expenses, from Milestone 2 on

Only main() prints and asks, so every other function can be tested by calling it. That's why 8.9 kept them apart. Each test passes a fixed date, so its result is the same every day.

Milestone 1: two happy paths

መልመጃ

The first two tests

Write two tests: one adds an expense to an empty list and checks the whole list, the other adds two food expenses and checks total_by_category.

Your code

from ledger import add_expense, total_by_category

# Test 1: add one expense with a fixed date to an empty list,
# and check the whole list.

# Test 2: add two food expenses and check total_by_category.

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

from ledger import add_expense, total_by_category


def test_add_expense():
    expenses = []
    add_expense(expenses, 250, "food", "ቡና", "2026-10-01")
    assert expenses == [
        {"date": "2026-10-01", "amount": 250, "category": "food", "note": "ቡና"},
    ]


def test_total_by_category():
    expenses = []
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    assert total_by_category(expenses) == {"food": 310}

Comparing the whole list checks all four keys at once. total_by_category returns a Counter, and a Counter equals a dict with the same keys and values, so == {"food": 310} works.

Milestone 2: the fixture

Most tests from here on need the same expenses: the three in 8.9's ledger.json.

መልመጃ

sample_expenses

This is the Milestone 1 file. Add the sample_expenses fixture, building the three expenses with add_expense, and rewrite both tests to use it.

Your code

from ledger import add_expense, total_by_category


def test_add_expense():
    expenses = []
    add_expense(expenses, 250, "food", "ቡና", "2026-10-01")
    assert expenses == [
        {"date": "2026-10-01", "amount": 250, "category": "food", "note": "ቡና"},
    ]


def test_total_by_category():
    expenses = []
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    assert total_by_category(expenses) == {"food": 310}

# Add a sample_expenses fixture: 8.9's three expenses, made with add_expense.
# Then make both tests use it.

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}

test_add_expense adds a fourth expense, but the next test still gets three: each test gets a fresh list (lesson 9.3).

Milestone 3: totals

መልመጃ

Totals, parametrized

This is the Milestone 2 file. Add one parametrized test for each category's total, including rent, which has no expenses, and a test for an empty ledger. A test can ask for a fixture and parametrize's names together, like def test_category_total(sample_expenses, category, expected):. pytest fills in each.

Your code

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


# Parametrize: each category's total, including one with no expenses.
# Then: the totals of an empty ledger.

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}

A Counter gives 0 for a category with no expenses (lesson 8.9), and an empty ledger has no totals: the empty input from lesson 9.4.

Milestone 4: bad amounts

8.9 asks again when the amount isn't a whole number, but it never said what a whole number must be. Here's the rule, written down now: an amount must be more than 0. An expense of -50 birr means nothing, and 0 adds nothing. Write the rule as a test first (lesson 9.4): an amount of 0 or -50 should raise ValueError.

መልመጃ

Bad amounts, test-first

This is the Milestone 3 file. Add a parametrized test with pytest.raises for the amounts 0 and -50. They should fail.

Your code

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


# Test that add_expense raises ValueError for an amount of 0 or -50.

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


@pytest.mark.parametrize("amount", [0, -50])
def test_bad_amount(sample_expenses, amount):
    with pytest.raises(ValueError):
        add_expense(sample_expenses, amount, "food", "ቡና", "2026-10-01")

Both fail with DID NOT RAISE.

ጥያቄ

The two bad-amount tests fail. What's wrong?

That's a real bug in v1, found by a test. Fix it where the rule belongs, in add_expense, and let main() handle the error.

መልመጃ

add_expense refuses bad amounts

This is ledger.py, with the Milestone 4 tests in their tab. Make add_expense raise ValueError("amount must be more than 0") before it appends. Then, in main(), call it inside try: print Not added: and the message on a ValueError, and save only when it worked (else, lesson 7.2).

Your code

import datetime
import json
from collections import Counter
from pathlib import Path

LEDGER_FILE = Path(__file__).parent / "ledger.json"


def load_expenses(path=LEDGER_FILE):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []


def save_expenses(expenses, path=LEDGER_FILE):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(expenses, f, ensure_ascii=False, indent=2)


def add_expense(expenses, amount, category, note, date=None):
    if date is None:
        date = datetime.date.today().isoformat()
    expenses.append({
        "date": date,
        "amount": amount,
        "category": category,
        "note": note,
    })


def total_by_category(expenses):
    totals = Counter()
    for e in expenses:
        totals[e["category"]] = totals[e["category"]] + e["amount"]
    return totals


def main():
    expenses = load_expenses()
    while True:
        command = input("Command: ")
        match command:
            case "add":
                amount = None
                while amount is None:
                    try:
                        amount = int(input("Amount: "))
                    except ValueError:
                        print("Amount must be a whole number of birr.")
                category = input("Category: ")
                note = input("Note: ")
                add_expense(expenses, amount, category, note)
                save_expenses(expenses)
                print(f"Added {amount} birr for {category}.")
            case "list":
                if not expenses:
                    print("No expenses yet.")
                for e in expenses:
                    print(e["date"], e["amount"], "birr", e["category"], e["note"])
            case "summary":
                for category, total in total_by_category(expenses).most_common():
                    print(f"{category}: {total} birr")
            case "quit":
                print("Goodbye!")
                break
            case _:
                print("Unknown command. Try add, list, summary, or quit.")


if __name__ == "__main__":
    main()

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import datetime
import json
from collections import Counter
from pathlib import Path

LEDGER_FILE = Path(__file__).parent / "ledger.json"


def load_expenses(path=LEDGER_FILE):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []


def save_expenses(expenses, path=LEDGER_FILE):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(expenses, f, ensure_ascii=False, indent=2)


def add_expense(expenses, amount, category, note, date=None):
    if amount <= 0:
        raise ValueError("amount must be more than 0")
    if date is None:
        date = datetime.date.today().isoformat()
    expenses.append({
        "date": date,
        "amount": amount,
        "category": category,
        "note": note,
    })


def total_by_category(expenses):
    totals = Counter()
    for e in expenses:
        totals[e["category"]] = totals[e["category"]] + e["amount"]
    return totals


def main():
    expenses = load_expenses()
    while True:
        command = input("Command: ")
        match command:
            case "add":
                amount = None
                while amount is None:
                    try:
                        amount = int(input("Amount: "))
                    except ValueError:
                        print("Amount must be a whole number of birr.")
                category = input("Category: ")
                note = input("Note: ")
                try:
                    add_expense(expenses, amount, category, note)
                except ValueError as error:
                    print(f"Not added: {error}.")
                else:
                    save_expenses(expenses)
                    print(f"Added {amount} birr for {category}.")
            case "list":
                if not expenses:
                    print("No expenses yet.")
                for e in expenses:
                    print(e["date"], e["amount"], "birr", e["category"], e["note"])
            case "summary":
                for category, total in total_by_category(expenses).most_common():
                    print(f"{category}: {total} birr")
            case "quit":
                print("Goodbye!")
                break
            case _:
                print("Unknown command. Try add, list, summary, or quit.")


if __name__ == "__main__":
    main()

The menu now refuses -50 too, and saves nothing:

import datetime
import json
from collections import Counter
from pathlib import Path

LEDGER_FILE = Path(__file__).parent / "ledger.json"


def load_expenses(path=LEDGER_FILE):
    try:
        with open(path, encoding="utf-8") as f:
            return json.load(f)
    except FileNotFoundError:
        return []


def save_expenses(expenses, path=LEDGER_FILE):
    with open(path, "w", encoding="utf-8") as f:
        json.dump(expenses, f, ensure_ascii=False, indent=2)


def add_expense(expenses, amount, category, note, date=None):
    if amount <= 0:
        raise ValueError("amount must be more than 0")
    if date is None:
        date = datetime.date.today().isoformat()
    expenses.append({
        "date": date,
        "amount": amount,
        "category": category,
        "note": note,
    })


def total_by_category(expenses):
    totals = Counter()
    for e in expenses:
        totals[e["category"]] = totals[e["category"]] + e["amount"]
    return totals


def main():
    expenses = load_expenses()
    while True:
        command = input("Command: ")
        match command:
            case "add":
                amount = None
                while amount is None:
                    try:
                        amount = int(input("Amount: "))
                    except ValueError:
                        print("Amount must be a whole number of birr.")
                category = input("Category: ")
                note = input("Note: ")
                try:
                    add_expense(expenses, amount, category, note)
                except ValueError as error:
                    print(f"Not added: {error}.")
                else:
                    save_expenses(expenses)
                    print(f"Added {amount} birr for {category}.")
            case "list":
                if not expenses:
                    print("No expenses yet.")
                for e in expenses:
                    print(e["date"], e["amount"], "birr", e["category"], e["note"])
            case "summary":
                for category, total in total_by_category(expenses).most_common():
                    print(f"{category}: {total} birr")
            case "quit":
                print("Goodbye!")
                break
            case _:
                print("Unknown command. Try add, list, summary, or quit.")


if __name__ == "__main__":
    main()

In the editor, press Escape then Tab to move on.

One answer per line, given to input() in order.

Milestone 5: files

save_expenses and load_expenses take a path (lesson 8.9), so a test can give them a file in tmp_path and never touch a real ledger.json.

መልመጃ

Round trips in tmp_path

This is the Milestone 4 file, and ledger.py now has the fix. Add a round trip of sample_expenses, Amharic notes included, and a test that loading a missing file gives [].

Your code

import pytest
from ledger import add_expense, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


@pytest.mark.parametrize("amount", [0, -50])
def test_bad_amount(sample_expenses, amount):
    with pytest.raises(ValueError):
        add_expense(sample_expenses, amount, "food", "ቡና", "2026-10-01")


# Save sample_expenses to a file in tmp_path and load it back.
# Then: loading a file that doesn't exist.

In the editor, press Escape then Tab to move on.

መፍትሄውን አሳይ

This is one way to solve it, not the only one. If yours prints the same thing, it works.

import pytest
from ledger import add_expense, load_expenses, save_expenses, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


@pytest.mark.parametrize("amount", [0, -50])
def test_bad_amount(sample_expenses, amount):
    with pytest.raises(ValueError):
        add_expense(sample_expenses, amount, "food", "ቡና", "2026-10-01")


def test_save_and_load(tmp_path, sample_expenses):
    path = tmp_path / "ledger.json"
    save_expenses(sample_expenses, path)
    assert load_expenses(path) == sample_expenses


def test_missing_file(tmp_path):
    assert load_expenses(tmp_path / "ledger.json") == []

Milestone 6: check the suite

Ten tests pass. Do they catch bugs? Below, ledger.py is the functions without main(), which the tests never call, and three copies each have one planted bug. The starting tests are the Milestone 5 file. Press Hunt.

Bug hunt

3 buggy versions of ledger.py are planted below. Good tests pass on the correct code and fail on every bug.

import pytest
from ledger import add_expense, load_expenses, save_expenses, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


@pytest.mark.parametrize("amount", [0, -50])
def test_bad_amount(sample_expenses, amount):
    with pytest.raises(ValueError):
        add_expense(sample_expenses, amount, "food", "ቡና", "2026-10-01")


def test_save_and_load(tmp_path, sample_expenses):
    path = tmp_path / "ledger.json"
    save_expenses(sample_expenses, path)
    assert load_expenses(path) == sample_expenses


def test_missing_file(tmp_path):
    assert load_expenses(tmp_path / "ledger.json") == []

In the editor, press Escape then Tab to move on.

Planted bugs

  • Totals skip the last expense
  • Amharic is saved as escapes
  • A missing file crashes
Show one answer

One set of tests that catches every bug, not the only one.

import pytest
from ledger import add_expense, load_expenses, save_expenses, total_by_category


@pytest.fixture
def sample_expenses():
    expenses = []
    add_expense(expenses, 120, "transport", "ታክሲ", "2026-09-28")
    add_expense(expenses, 250, "food", "ምሳ", "2026-09-29")
    add_expense(expenses, 60, "food", "ቡና", "2026-09-30")
    return expenses


def test_add_expense(sample_expenses):
    add_expense(sample_expenses, 40, "transport", "ባጃጅ", "2026-10-01")
    assert len(sample_expenses) == 4
    assert sample_expenses[-1] == {
        "date": "2026-10-01",
        "amount": 40,
        "category": "transport",
        "note": "ባጃጅ",
    }


def test_total_by_category(sample_expenses):
    assert total_by_category(sample_expenses) == {"food": 310, "transport": 120}


@pytest.mark.parametrize("category, expected", [
    ("food", 310),
    ("transport", 120),
    ("rent", 0),
])
def test_category_total(sample_expenses, category, expected):
    assert total_by_category(sample_expenses)[category] == expected


def test_empty_ledger():
    assert total_by_category([]) == {}


@pytest.mark.parametrize("amount", [0, -50])
def test_bad_amount(sample_expenses, amount):
    with pytest.raises(ValueError):
        add_expense(sample_expenses, amount, "food", "ቡና", "2026-10-01")


def test_save_and_load(tmp_path, sample_expenses):
    path = tmp_path / "ledger.json"
    save_expenses(sample_expenses, path)
    assert load_expenses(path) == sample_expenses


def test_missing_file(tmp_path):
    assert load_expenses(tmp_path / "ledger.json") == []


def test_amharic_stays_readable(tmp_path, sample_expenses):
    path = tmp_path / "ledger.json"
    save_expenses(sample_expenses, path)
    assert "ቡና" in path.read_text(encoding="utf-8")

Caught 2 of 3. The round trip saved ቡና and loaded it back, so why did it miss the escapes? Predict:

ውጤቱን ገምቱ

Decide before you look. Guessing wrong is how this sticks.

import json

text = json.dumps(["ቡና"])
print(json.loads(text) == ["ቡና"])
print("ቡና" in text)
Pick the output

Python prints

True
False

An escaped note loads back as the same text (lesson 8.4), so the round trip is equal either way. Only the file's text shows the escapes: it has no ቡና in it at all.

If you picked True twice, you trusted the round trip to see the file. It only sees what comes back. To test how the file looks, read it: add a test that saves sample_expenses to tmp_path and checks "ቡና" in path.read_text(encoding="utf-8") (lesson 8.2). Hunt again: 3 of 3.

Run it for real

On a computer, the course's exercises repo has this project in 09-ledger-tests: 8.9's ledger.py and a test_ledger.py with Milestone 1. Open it in VS Code or a Codespace (lesson 8.8), and in a terminal in that folder run:

uv run pytest

Its pyproject.toml lists pytest as a dev dependency: a package for working on the project, not for running it. The first time, uv installs it into the folder's .venv (lesson 8.8), then runs it. Without uv, run python -m pytest in an environment that has pytest. pytest prints a header first, naming your Python and the folder (that's why the browser uses -q), then the same dots and summary, and leaves a .pytest_cache folder you can ignore. Build the suite milestone by milestone, and run it after every change to ledger.py.

ዋና ዋና ነጥቦች

  • Test the functions, not the menu: only main() prints and asks, so everything else can be called with known values.
  • Fixed dates and tmp_path make tests give the same result every day and never touch real data.
  • A failing test found a real bug in v1. Check the requirement, then fix the code where the rule belongs.
  • A round trip can't see how a file looks. Check the suite with planted bugs; read the file when its text matters.

That's every lesson written so far

New lessons appear here as they're written, free like this one. The Telegram channel (opens in a new tab) announces each one.

Want video lessons, live sessions, and the community group too? See the paid Python course in Telegram።