|
| 1 | +--- |
| 2 | +parent: "Testing Your Code" |
| 3 | +grand_parent: Programming for Modelling and Data Analysis |
| 4 | +nav_order: 1 |
| 5 | +--- |
| 6 | + |
| 7 | +# Writing unit tests |
| 8 | + |
| 9 | +## Why unit tests? |
| 10 | + |
| 11 | +When writing code, it is important to ensure that it works as expected. One way to do this is by writing *unit tests*. Unit tests are small pieces of code that test individual units or components of your code to verify that they behave as intended. |
| 12 | + |
| 13 | +Unit tests are important for several reasons: |
| 14 | +1. **Catch Bugs Early**: By testing individual components, you can catch bugs early in the development process, making them easier and cheaper to fix. |
| 15 | +2. **Facilitate Change**: Unit tests provide a safety net when making changes to your code. If you modify a component and the tests fail, you know immediately that something is wrong. |
| 16 | +3. **Documentation**: Unit tests can serve as documentation for your code. They show how the code is intended to be used and what its expected behavior is. |
| 17 | +4. **Improve Design**: Writing tests can help you think about your code's design and architecture, leading to better-structured and more maintainable code. |
| 18 | + |
| 19 | +## `unittest` module |
| 20 | + |
| 21 | +In Python, you can use the built-in `unittest` framework to write and run unit tests. Here's a simple example to illustrate how to create and run unit tests. |
| 22 | + |
| 23 | +```python |
| 24 | +import unittest |
| 25 | + |
| 26 | +def is_even(x: int) -> bool: |
| 27 | + return x % 2 == 0 |
| 28 | + |
| 29 | +class TestIsEven(unittest.TestCase): |
| 30 | + def test_even_number_returns_true(self): |
| 31 | + self.assertTrue(is_even(4)) |
| 32 | + |
| 33 | + def test_odd_number_returns_false(self): |
| 34 | + self.assertFalse(is_even(5)) |
| 35 | + |
| 36 | +if __name__ == "__main__": |
| 37 | + unittest.main() # allows running this file directly |
| 38 | +``` |
| 39 | + |
| 40 | +In this example, we define a simple function `is_even` that checks if a number is even. We then create a test case class `TestIsEven` that inherits from `unittest.TestCase`. Inside this class, we define two test methods: `test_even_number_returns_true` and `test_odd_number_returns_false`, which test the behavior of the `is_even` function. |
| 41 | + |
| 42 | +To run the tests, you can execute the script directly, and the `unittest` framework will run all the test methods defined in the `TestIsEven` class. |
| 43 | + |
| 44 | +## Organizing unit tests |
| 45 | + |
| 46 | +Most tests written with the `unittest` framework are organized as classes that derive from `unittest.TestCase`. |
| 47 | + |
| 48 | +- Group related tests into a `Test...` class. The class name should start with `Test` and describe what is being tested, for example `class TestMathUtils(unittest.TestCase):` or `class TestStringHelpers(unittest.TestCase):`. |
| 49 | +- Put individual checks into methods whose names start with `test_`. The test runner discovers and runs any method beginning with `test_`. |
| 50 | +- Keep each test method focused: one logical assertion or behavior per test method. This makes failures easier to interpret. |
| 51 | +- Name test methods clearly to explain the expected behavior, for example `test_add_positive_numbers`, `test_raises_on_invalid_input`, `test_returns_empty_list_when_no_items`. |
| 52 | +- Avoid putting multiple unrelated assertions in a single test method — if you must, prefer splitting into several test methods. |
| 53 | +- Keep tests independent: they should not rely on global state left by other tests. |
| 54 | + |
| 55 | +Example layout inside a package: |
| 56 | + |
| 57 | +``` |
| 58 | +project/ |
| 59 | + package/ |
| 60 | + utils.py |
| 61 | + other.py |
| 62 | + tests/ |
| 63 | + test_utils.py # contains TestUtils (Test... classes and test_ methods) |
| 64 | + test_other.py |
| 65 | +``` |
| 66 | + |
| 67 | +With this structure and clear naming, test discovery is reliable and the VS Code test explorer and other tools will show readable test names that map back to your code. |
| 68 | + |
| 69 | + |
| 70 | +## Available assertions |
| 71 | + |
| 72 | +The `unittest.TestCase` class provides several assertion methods to check for different conditions in your tests. Here are some commonly used assertions: |
| 73 | + |
| 74 | +- `self.assertEqual(a, b)`: Check that `a` is equal to `b`. |
| 75 | +- `self.assertNotEqual(a, b)`: Check that `a` is not equal to `b`. |
| 76 | +- `self.assertTrue(x)`: Check that `x` is `True`. |
| 77 | +- `self.assertFalse(x)`: Check that `x` is `False`. |
| 78 | +- `self.assertIsNone(x)`: Check that `x` is `None`. |
| 79 | +- `self.assertIsNotNone(x)`: Check that `x` is not `None`. |
| 80 | +- `self.assertIn(a, b)`: Check that `a` is in `b`. |
| 81 | +- `self.assertNotIn(a, b)`: Check that `a` is not in `b`. |
| 82 | +- `self.assertRaises(exception, callable, *args, **kwargs)`: Check that calling `callable` with the given arguments raises the specified `exception`. |
| 83 | + |
| 84 | +Below are short examples that show how the most common assertions are used in practice. |
| 85 | + |
| 86 | +```python |
| 87 | +class TestAssertions(unittest.TestCase): |
| 88 | + def test_equal_and_not_equal(self): |
| 89 | + self.assertEqual(2 + 2, 4) |
| 90 | + self.assertNotEqual(2 + 2, 5) |
| 91 | + |
| 92 | + def test_true_false(self): |
| 93 | + self.assertTrue(1 < 2) |
| 94 | + self.assertFalse(1 > 2) |
| 95 | + |
| 96 | + def test_is_none(self): |
| 97 | + x = None |
| 98 | + self.assertIsNone(x) |
| 99 | + |
| 100 | + def test_in_and_not_in(self): |
| 101 | + self.assertIn('py', 'python') |
| 102 | + self.assertNotIn('java', 'python') |
| 103 | + |
| 104 | + def test_combined_examples(self): |
| 105 | + items = [1, 2, 3] |
| 106 | + self.assertEqual(len(items), 3) |
| 107 | + self.assertTrue(isinstance(items, list)) |
| 108 | + |
| 109 | +``` |
| 110 | + |
| 111 | +### Using assertRaises |
| 112 | + |
| 113 | +Testing that code raises the expected exception is common. `unittest` provides two ways to assert exceptions: |
| 114 | + |
| 115 | +1. The context-manager form (preferred when you want to run a block of code and optionally inspect the exception object). |
| 116 | +2. The callable form, where you pass the callable and its arguments to `assertRaises`. |
| 117 | + |
| 118 | +Examples: |
| 119 | + |
| 120 | +```python |
| 121 | +class TestExceptions(unittest.TestCase): |
| 122 | + # Context manager form (recommended for readability) |
| 123 | + def test_divide_by_zero_context(self): |
| 124 | + with self.assertRaises(ZeroDivisionError) as cm: |
| 125 | + _ = 1 / 0 |
| 126 | + # optional: inspect the exception |
| 127 | + self.assertIn('division', str(cm.exception)) |
| 128 | + |
| 129 | + # Callable form |
| 130 | + def test_divide_by_zero_callable(self): |
| 131 | + |
| 132 | + def divide_by_zero(): |
| 133 | + return 1 / 0 |
| 134 | + |
| 135 | + self.assertRaises(ZeroDivisionError, divide_by_zero) |
| 136 | + |
| 137 | + # Another example: check that invalid index raises IndexError |
| 138 | + def test_index_error(self): |
| 139 | + lst = [] |
| 140 | + with self.assertRaises(IndexError): |
| 141 | + _ = lst[0] |
| 142 | + |
| 143 | +``` |
| 144 | + |
| 145 | +Notes and tips: |
| 146 | +- Prefer the context-manager form when you need to run several lines and/or inspect the exception object. |
| 147 | +- Use the callable form for short one-liners when that is clearer. |
| 148 | +- Be explicit about the specific exception you expect — don't assert a broad Exception unless truly necessary. |
| 149 | + |
| 150 | + |
| 151 | + |
| 152 | +<hr/> |
| 153 | + |
| 154 | +Published under [Creative Commons Attribution-NonCommercial-ShareAlike](https://creativecommons.org/licenses/by-nc-sa/4.0/) license. |
0 commit comments