Testing with unittest & pytest
Checking access...
Why Test?
Testing ensures your code works correctly and continues to work as you make changes.
unittest
Python’s built-in testing framework.
def add(a, b): return a + b
def divide(a, b): if b == 0: raise ValueError("Cannot divide by zero") return a / bimport unittestfrom calculator import add, divide
class TestCalculator(unittest.TestCase):
def test_add_integers(self): self.assertEqual(add(2, 3), 5)
def test_add_floats(self): self.assertAlmostEqual(add(0.1, 0.2), 0.3, places=1)
def test_divide_normal(self): self.assertEqual(divide(10, 2), 5.0)
def test_divide_by_zero(self): with self.assertRaises(ValueError): divide(10, 0)
if __name__ == "__main__": unittest.main()python -m unittest test_calculator.py# ....# Ran 4 tests in 0.001s# OKunittest Assertions
self.assertEqual(a, b) # a == bself.assertNotEqual(a, b) # a != bself.assertTrue(x) # bool(x) is Trueself.assertFalse(x) # bool(x) is Falseself.assertIs(a, b) # a is bself.assertIsNone(x) # x is Noneself.assertIn(item, list) # item in listself.assertAlmostEqual(a, b) # floats within toleranceself.assertRaises(Exc, func) # func raises Excself.assertIsInstance(obj, cls)Setup and Teardown
import unittest
class TestDatabase(unittest.TestCase):
@classmethod def setUpClass(cls): """Run once before all tests.""" cls.connection = "connected"
@classmethod def tearDownClass(cls): """Run once after all tests.""" cls.connection = None
def setUp(self): """Run before each test.""" self.data = {"key": "value"}
def tearDown(self): """Run after each test.""" self.data.clear()
def test_insert(self): self.data["new"] = "data" self.assertIn("new", self.data)
def test_delete(self): del self.data["key"] self.assertNotIn("key", self.data)pytest
A more expressive testing framework (third-party, very popular).
pip install pytestimport pytest
def test_addition(): assert add(2, 3) == 5
def test_division_by_zero(): with pytest.raises(ValueError, match="Cannot divide"): divide(10, 0)
# Parametrized tests@pytest.mark.parametrize("a, b, expected", [ (2, 3, 5), (0, 0, 0), (-1, 1, 0), (0.5, 0.5, 1.0),])def test_add_params(a, b, expected): assert add(a, b) == expected
# Fixtures@pytest.fixturedef sample_data(): """Provide test data.""" return {"name": "Alice", "scores": [85, 92, 78]}
def test_average(sample_data): scores = sample_data["scores"] assert sum(scores) / len(scores) == 85pytest Fixtures
import pytest
@pytest.fixturedef db_connection(): """Set up and tear down database.""" conn = {"connected": True} # Setup yield conn # Provide to test conn["connected"] = False # Teardown
@pytest.fixture(scope="module")def config(): """Shared across all tests in the module.""" return {"host": "localhost", "port": 5432}
def test_query(db_connection): assert db_connection["connected"] is True
def test_config(config): assert config["host"] == "localhost"pytest Features
# Temporary directoriesdef test_file_creation(tmp_path): d = tmp_path / "subdir" d.mkdir() f = d / "test.txt" f.write_text("hello") assert f.read_text() == "hello"
# Monkeypatchingdef test_env_var(monkeypatch): monkeypatch.setenv("DATABASE_URL", "sqlite:///test.db") import os assert os.environ["DATABASE_URL"] == "sqlite:///test.db"
# Capturing outputdef test_print(capsys): print("Hello!") captured = capsys.readouterr() assert captured.out == "Hello!\n"
# Mockingfrom unittest.mock import Mock
def test_mock(): mock = Mock(return_value=42) assert mock() == 42 assert mock.calledRunning pytest
# Run all testspytest
# Verbosepytest -v
# Run specific filepytest test_calculator.py
# Run specific testpytest test_calculator.py::test_addition
# Run tests matching patternpytest -k "add"
# Stop on first failurepytest -x
# Show print statementspytest -s
# Coverage reportpip install pytest-covpytest --cov=myapp tests/Test Organization
project/├── src/│ ├── calculator.py│ └── database.py└── tests/ ├── test_calculator.py └── test_database.pyTest-Driven Development (TDD)
- Write a failing test
- Write minimal code to make it pass
- Refactor
# Step 1: Write testdef test_is_palindrome(): assert is_palindrome("racecar") is True assert is_palindrome("hello") is False
# Step 2: Make it passdef is_palindrome(s): return s == s[::-1]
# Step 3: Refactor (if needed)Key Takeaways
unittestis built-in;pytestis more expressive and commonly preferredassertEqual,assertTrue,assertRaisesfor unittest assertionspytest.raises()andpytest.mark.parametrize()for concise tests@pytest.fixturefor setup/teardown withyieldtmp_path,monkeypatch,capsysfor testing edge cases- Organize tests in a
tests/directory mirroring your source - Run with
pytest -vfor verbose output,pytest --covfor coverage - TDD: red (failing test) → green (passing) → refactor