✦ Python Intermediate & OOP Code quality

L2 · Concept 04 · Code Quality & Testing

Unit Testing with unittest
Test Your Code Before Bugs Surprise You.

Learn to write small automatic checks that confirm expected behavior before a change turns into a surprise.

Learning goalWrite simple unittest checks with clear assertions, run them locally, and use normal and edge cases to protect expected behavior.
01 Learn02 Work locally03 Check your understanding

Learning as a guest

Learning as a guest? Save your progress!

Create a free account to learn with Sloby AI, check your understanding, and track your progress.

The entire platform is free throughout 2026. Foundations stays free forever.

Tests check expectations automatically.

Debugging helps us find the cause of a problem we have already noticed. Automated tests check behavior we expect and can warn us when a later change accidentally breaks something that worked before.

# Manual check
print(izracunaj_popust(100, 10))

# Automatic check inside a test
self.assertEqual(izracunaj_popust(100, 10), 90)

A manual check asks you to read and judge an output. An automatic assertion compares the actual result with the expected result and reports whether they match.

Tests do not prove that a program has no bugs. They verify the specific cases you choose, so choosing useful examples matters.

↳

A unit is small: A unit test usually checks one focused part of a program, such as a function or method, in a situation you can describe clearly.

Your first unittest
import unittest

def saberi(a: int, b: int) -> int:
    return a + b


class TestSabiranje(unittest.TestCase):
    def test_dva_pozitivna_broja(self):
        self.assertEqual(saberi(2, 3), 5)


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

We import the standard unittest library. unittest.TestCase gives our test class assertion methods. A method beginning with test_ is discovered as a test, and assertEqual compares an actual result with an expected value. unittest.main() is useful when running this file directly, but tests can also run through python -m unittest.

Keep implementation and tests separate
moj_projekat/
├── kalkulator.py
└── test_kalkulator.py

# kalkulator.py
def saberi(a: int, b: int) -> int:
    return a + b

# test_kalkulator.py
import unittest
from kalkulator import saberi

class TestKalkulator(unittest.TestCase):
    def test_sabiranje(self):
        self.assertEqual(saberi(4, 6), 10)

Separating the implementation from its tests keeps each file focused. From the project folder, run python -m unittest -v; on some systems use python3 -m unittest -v. The -v option shows more detail, and unittest normally discovers files named like test_kalkulator.py or other test*.py files.

Arrange → Act → Assert
def test_popust_deset_posto(self):
    # Arrange
    cena = 200
    popust = 10

    # Act
    rezultat = izracunaj_popust(cena, popust)

    # Assert
    self.assertEqual(rezultat, 180)

Arrange prepares input data, Act runs the behavior, and Assert compares the actual result with the expected result. These comments are useful while learning, but a clear test name and clear expectation are often more valuable than many comments.

Three basic assertions
self.assertEqual(saberi(2, 3), 5)
self.assertTrue(10 > 5)
self.assertFalse(10 < 5)

assertEqual checks that two values are equal. assertTrue checks a true expression, and assertFalse checks a false expression. For a specific expected number or text, assertEqual is usually the clearest choice. We will meet assertRaises in the next lesson about exceptions.

Run tests from the VS Code terminal
python -m unittest -v
python -m unittest discover -v

# Or, on some systems:
python3 -m unittest -v
python3 -m unittest discover -v

Run the command from the appropriate project folder. Discovery and its exact output can depend on the file structure and Python version, but the default test-file pattern is test*.py.

Read a test result carefully

  1. OK means every executed assertion passedAn OK result means the test runner completed its checks and none of their assertions failed.
  2. FAIL means an assertion was not trueFor example, self.assertEqual(10, 12) runs normally but fails because the values are different.
  3. ERROR means the test could not finish normallyFor example, rezultat = 10 / 0 raises an unexpected exception before a later assertion can run. FAIL and ERROR are different clues, not proof that Python itself is broken.
  4. Use normal and edge casesFor a discount function, test 0%, 10%, 100%, and a price of 0. An edge case is a boundary situation that one ordinary example may miss.
  5. Let tests protect against regressionsA regression happens when a later change breaks behavior that used to work. A test can report FAIL, then debugging helps you find and fix the cause. Do not change an expected result just to make a failing test pass; first decide what behavior is correct.

Common beginner testing mistakes

  • Testing only one exampleOne passing example gives limited confidence. Add typical and edge cases that matter for the function.
  • Forgetting the test_ prefixA method without the expected test_ prefix may not be discovered and run.
  • Running from the wrong folderCheck the terminal folder and file names before assuming unittest cannot find your tests.
  • Changing an expectation only to get OKA test name should describe what it checks. Investigate whether the implementation or the expected result is wrong before changing either one.

My First Automated Tests

Local VS Code practice

Download a small discount-calculator project, run its starter tests, and use one intentional FAIL to practice finding and fixing a logic error.

Download project ZIP
Project files
unittest_prvi_testovi/
├── kalkulator.py
├── test_kalkulator.py
├── README_SR.md
└── README_EN.md

The starter project contains three small price functions and three tests. One test FAIL is intentional; the other starter tests pass without ERROR.

Work through the tests locally

  1. Open the extracted folder in VS CodeRead the matching README file, then use the integrated terminal from the unittest_prvi_testovi folder.
    python3 -m unittest -v
  2. Investigate the intentional FAILCompare the actual discount result with the expected value before changing code. The goal is to repair the logic, not to weaken the test.
↳

Run again after a focused change: python3 -m unittest -v On systems where Python is called <code>python</code>, use <code>python -m unittest -v</code>. Keep the tests after the fix so they can reveal a later regression.

A testing habit to keep

  • Read FAIL before changing codeCompare the actual and expected values, then decide whether the behavior or the expectation needs investigation.

What will be the result of running this test?

Assume unittest discovers and runs this test method.

import unittest


class TestBrojevi(unittest.TestCase):
    def test_sabiranje(self):
        self.assertEqual(10, 12)
Choose the output

Build confidence independently

Plan three tests for a square function

Do not write a complete solution immediately. First decide which inputs and expected results would give useful evidence.

Your goalWrite three meaningful tests for kvadrat: one positive number, zero, and one negative number. For each test, explain the expected result.
def kvadrat(broj: int) -> int:
    return broj * broj

Work in your local VS Code folder

Progressive hints
  • Start by writing the input and the exact expected output for one positive number.
  • Ask what multiplying zero by zero returns.
  • A negative number squared is a useful check because the sign changes in the result.

Ask Sloby for one guiding question about an assertion or edge case; do not ask it to write all three finished tests for you.

Sloby, the Python AI Tutor mascot

Your learning mentor

Sign in to ask Sloby

Create a free account or sign in to get AI guidance while you learn.

Sign in to check your understanding

Create a free account or sign in to receive AI feedback and save your learning progress.

Python AI Tutor · Learn by understanding, not just by running code.