|
| 1 | +## Unit Tests |
| 2 | + |
| 3 | +### Advantages of unit testing: |
| 4 | + |
| 5 | +- Unit tests ensure that the code, as written, is correct, and executes |
| 6 | + properly. |
| 7 | +- They communicate the intention of the creator of the code, how the code is |
| 8 | + expected to behave, in its expected use-case. |
| 9 | + |
| 10 | +- Writing unit tests can reveal weakensses in our implementations, and lead us |
| 11 | + to better design decisions: |
| 12 | + - If the test requires excessive setup, the unit may be dependent on too many |
| 13 | + external variables. |
| 14 | + - If the test requires many assertions, the unit may be doing too many things |
| 15 | + / have too many side-effects. |
| 16 | + - If the unit is very difficult to test, it will likely be difficult to |
| 17 | + understand and maintain. Refactoring code to make it easier to test often |
| 18 | + leads us to write better code overall. |
| 19 | + |
| 20 | +### Guidelines for unit testing: |
| 21 | + |
| 22 | +- Unit tests live alongside the code they test, in a /tests folder. |
| 23 | + - It looks like `unittest discover` cannot find TestCase classes within source |
| 24 | + files. |
| 25 | + |
| 26 | +- Test files should be named `test_{{file under test}}.py`, so that stdlib |
| 27 | + unittest can find them easily. |
| 28 | + |
| 29 | +- test\_.py files should match your source files (file-under-test) one-to-one, |
| 30 | + and contain only tests for code in the file-file-under test. |
| 31 | + |
| 32 | +- Consider using the stdlib `unittest.TestCase` and other stdlib tools instead |
| 33 | + of pytest. |
| 34 | + - Allows running unit tests for diagnostics in production environments, |
| 35 | + without installing additional packages. |
| 36 | + |
| 37 | +- Keep it simple! If a test-case requires extra setup and external tools, It may |
| 38 | + be more appropriate as an external test, instead of in the unit tests |
| 39 | + |
| 40 | +- Test single units of code! A single Function, or a single attribute or method |
| 41 | + on a class. Not the interactions between them. (see mocking and patching) |
| 42 | + |
| 43 | +#### Importing in test files: |
| 44 | + |
| 45 | +- Use relative imports, from the file under test: |
| 46 | + |
| 47 | + ``` |
| 48 | + from ..file_under_test import MyClass |
| 49 | + ``` |
| 50 | + |
| 51 | + - This ensures that the test file always imports from its source file, and |
| 52 | + does not accidentally import from an installed module. |
| 53 | + |
| 54 | +- Only import from the file-under-test, unless absolutely necessary: |
| 55 | + - If your source runs `from pandas import DataFrame` and you need a DataFrame |
| 56 | + for a test, import it `from ..file_under_test import DataFrame` NOT |
| 57 | + `from pandas import DataFrame` in the test file. |
| 58 | + - This has some important implications for mocking/patching to elaborate on. |
| 59 | + |
| 60 | +#### Running unit tests: |
| 61 | + |
| 62 | +- Pytest is great for running tests in your development environments! |
| 63 | + `pytest {{path/to/source}}` |
| 64 | +- stdlib's unittest can be used in environments where pytest is not available: |
| 65 | + - To use unittest to run tests in your source folder, from your package root, |
| 66 | + use |
| 67 | + `python -m unittest discover --start-folder {{source folder}} --top-level-directory .` |
| 68 | + - To use unittest to run tests from an installed package (outside of your |
| 69 | + source repository), use `python -m unittest discover -s {{module.name}}` |
| 70 | + |
| 71 | +#### Mocking and Patching to Isolate the code under test: |
| 72 | + |
| 73 | +- When the unit you are testing touches any external unit (usually something you |
| 74 | + imported, or another unit that has its own tests), the external unit should be |
| 75 | + Patched, replacing it with a Mock for the durration of the test. |
| 76 | + |
| 77 | +- Verify that the external unit is called with the expected input |
| 78 | +- Verify that any value returned from the external unit is utilized as expected. |
| 79 | + |
| 80 | + ``` |
| 81 | + @patch(f'{SRC}.otherfunction', autospec=true) |
| 82 | + def test_myfunction(t, external_unit: Mock): |
| 83 | + ret = myfunction() |
| 84 | + external_unit.assert_called_with('input from myfunction') |
| 85 | + t.assertIs(ret, external_unit.return_value) |
| 86 | +
|
| 87 | + ``` |
| 88 | + |
| 89 | +- Consider what needs to be mocked, and the level of isolation your unit test |
| 90 | + really needs. |
| 91 | + - Anything imported into the module you are testing should probably be mocked. |
| 92 | + - external units with side-effects, or which do a lot of computation should |
| 93 | + probably be mocked. |
| 94 | + - Some people prefer to never test `_private` attributes. |
0 commit comments