@@ -10,27 +10,146 @@ parent: Principles
1010
1111# Testing recommendations
1212
13- ## Outside-In Tests
14- * live outside of source code, in the tests/ directory
15- * Describe the various types of outsid-in tests (integration, fuzz, e2e, API)
16- * Reference topical guides
17- * Provide suggestions for testing categories
13+ In the guide, we will classify two kingdoms of test: external and internal.
14+ External tests view the module from the perspective of a user of the module, and
15+ are concerned that the public-facing features behave as expected. Internal tests
16+ view the module from the perspective of code inside of the module, and ensure
17+ that the components that make up our package work as expected, and interact with
18+ each other properly.
1819
20+ ### Any test case is better than none
21+
22+ When in doubt, write the test that makes sense at the time.
23+
24+ - Test critical behaviors, features, and logic
25+ - Write clear, expressive, well documented tests
26+ - Tests are documentation of the developer's intentions
27+ - Good tests make it clear what they are testing and how
28+
29+ While you are learning, and writing your first test suites, try not to get
30+ bogged down in the taxonomy of test types. As you write and use your test suite,
31+ the reason for classifying and sorting some types of tests into different test
32+ suites will become apparent.
33+
34+ ### As long as that test is correct...
35+
36+ It can be surprisingly easy to write a test that passes when it should fail,
37+ especially when using complicated mocks and fixtures. The best way to avoid this
38+ is to deliberately break the code you are testing, hard-code a failure, and run
39+ the test-case to make sure it fails when the code is broken.
40+
41+ - Check that your test fails when it should!
42+ - Keep It Simple: Excessive use of mocks and fixtures can make it difficult to
43+ know if our test is running the code we expect it to.
44+ - Test one thing at a time: A single test should test a single behavior, and it
45+ is better to write many test cases for a single function or class, than one
46+ giant case.
47+
48+ ## External or outside-in testing
49+
50+ A good place to start writing tests is from the perspective of a user of your
51+ module or library, as described in the [ Test
52+ Tutorial] ({% link pages/tutorials/test.md %}), and [ Testing with pytest
53+ guide] ({% link pages/guides/pytest.md %}). These test cases live outside your
54+ code, and include many styles or types of test that you may have heard of
55+ (behavioral, fuzz, end-to-end, feature, etc., etc.).
56+
57+ {: .highlight-title }
58+
59+ > A note to new test developers:
60+ >
61+ > This is a good place to pause and go write some tests. The rest of these
62+ > principles apply to more advanced test development. As you gain experience and
63+ > your test suite(s) grow, taxonomy of test cases, the and the use/need for
64+ > different kinds of tests will become more clear.
65+
66+ ### Taxonomy of outside-in tests
67+
68+ A non-exhaustive discussion of some common types of tests.
69+
70+ ^_ ^ Dont Panic ^_ ^
71+
72+ Depending on your project, you may not need many, or most of these kinds of
73+ tests.
74+
75+ - A library project probably does not need to test integration with
76+ microservices.
77+ - A library with no 3rd party dependencies, does not need test them.
78+ - Fuzz testing is for critical code, that many users rely on.
79+
80+ #### Behavioral, Feature, or Functional Tests:
81+
82+ High-level tests, which ensure a specific feature works. Used for testing things
83+ like:
84+
85+ - Loading a file works
86+ - Setting a debug flag results in debug messages being printed
87+ - A configuration option affects the behavior of the code as expected
88+
89+ #### Fuzz Tests
90+
91+ Fuzz tests attempt to test the full range of possible inputs to a function. They
92+ are good for finding edge-cases, where what should be valid input causes a
93+ failure. [ Hypothesis] ( https://hypothesis.readthedocs.io/en/latest/ ) is an
94+ excellent tool for this, and a lot of fun to use.
95+
96+ - SLOW TESTS: fuzz tests can take a very long time to run, and should usually be
97+ placed in a test suite which is run separately from faster tests.
98+ [ see: fail fast] ( https://en.wikipedia.org/wiki/Fail-fast_system )
99+ - Reserve fuzz testing for the few critical functions, where it really matters.
100+
101+ #### Integration Tests
102+
103+ The word "Integration" is a bit overloaded, and can refer to many levels of
104+ interaction between your code, its dependencies, and external systems.
105+
106+ - Code level
107+ - Test the integration between your software and external / 3rd party
108+ dependencies.
109+ - Low-level testing of your code-base, where you run the code imported from
110+ dependencies without mocking it.
111+
112+ - Environment level
113+ - Testing that your software works in the environments you plan to run it in.
114+ - Running inside of a docker container
115+ - Using GPU's or other specialized hardware
116+ - Deploying it to cloud servers
117+
118+ - System level
119+ - Testing that it interacts with other software in a larger system.
120+ - Interactions with other services, on local or cloud-based platforms
121+ - Micro-service, Database, or API connections and interactions
122+
123+ #### End to End Tests
124+
125+ The slowest, and most brittle, of all tests. Here, you set up an entire
126+ production-like system, and run tests against it.
127+
128+ - Create a Dev / Testing / Staging environment, and run tests against it to make
129+ sure everything works together
130+ - Fake user input, using tools like
131+ [ Selenium] ( https://www.selenium.dev/documentation/ )
132+ - Processing data from a pre-loaded test database
133+ - Manual QA testing
19134
20135## Unit Tests
21136
137+ Internal tests, which test that individual units/components of the code behave
138+ as expected in isolation. Some examples of units are: A single function, an
139+ attribute of an object, a method or property of a class.
140+
22141### Advantages of unit testing:
23142
24143Unit tests ensure that the code, as written, is correct, and executes properly.
25- they communicate the intention of the creator of the code, how the code is
144+ They communicate the intention of the creator of the code, how the code is
26145expected to behave, in its expected use-case.
27146
28- Unit tests should be simple, isolated, and run very quickly. Which allows us to
147+ Unit tests should be simple, isolated, and run very quickly. This allows us to
29148run them quickly, while we make changes to the code (even automatically, each
30149time we save a file for example) to ensure our changes did not break anything...
31150or only break what we expected to.
32151
33- Writing unit tests can reveal weakensses in our implementations, and lead us to
152+ Writing unit tests can reveal weaknesses in our implementations, and lead us to
34153better design decisions:
35154
36155- If the test requires excessive setup, the unit may be dependent on too many
@@ -46,13 +165,13 @@ better design decisions:
46165Unit tests are considered "low level", and used for [ Isolation Testing] ( ) . Not
47166all projects need full unit test coverage, some may not need unit tests at all.
48167
49- - When your project matures enough to justify the work! higher -level testing is
50- often sufficient for small projects, which are not part of critical
168+ - When your project matures enough to justify the work! Higher -level testing is
169+ often sufficient for small projects which are not part of critical
51170 infrastructure.
52171
53- - When you identify a critical part of the code-base, parts that are especially
54- prone to breaking, Use unit tests to ensure that code continues to behave as
55- designed.
172+ - When you identify a critical part of the code-base, or parts that are
173+ especially prone to breaking, use unit tests to ensure that code continues to
174+ behave as designed.
56175
57176- When other projects start to depend heavily on your library, thorough unit
58177 testing helps ensure the reliability of your code for your users.
@@ -65,34 +184,34 @@ all projects need full unit test coverage, some may not need unit tests at all.
65184
66185- Unit tests live alongside the code they test, in a /tests folder. They should
67186 be in a different directory than higher-level tests (integration, e2e,
68- behavioral, etc.) So that they can be run quickly before the full test suite,
69- and to avoid confusing them.
187+ behavioral, etc) so that they can be run quickly before the full test suite,
188+ and to avoid confusing them with other types of tests .
70189
71190- Test files should be named ` test_{{file under test}}.py ` , so that test runners
72191 can find them easily.
73192
74193- test\_ .py files should match your source files (file-under-test) one-to-one,
75- and contain only tests for code in the file-file- under test. The code in
194+ and contain only tests for code in the file-under test. The code in
76195 ` mymodule/source.py ` is tested by ` mymodule/tests/test_source.py ` .
77196
78197- Keep it simple! If a test-case requires extra setup and external tools, It may
79198 be more appropriate as an external test, instead of in the unit tests
80199
81200- Avoid the temptation to test edge-cases! Focus your unit tests on the
82- "happy-path". The UT should describe the expected and officially supported
83- usage of the code under test.
201+ "happy-path". The Unit test should describe the expected and officially
202+ supported usage of the code under test.
84203
85- - Isolation: Test single units of code! A single Function , or a single attribute
204+ - Isolation: Test single units of code! A single function , or a single attribute
86205 or method on a class. If you have two units (classes, functions, class
87206 attributes) with deeply coupled behavior, it is better to test them
88- individually, using mocking and patching, instead of testing both in a single
207+ individually using mocking and patching, instead of testing both in a single
89208 test. This makes refactoring easier, helps you understand the interactions
90209 between units, and will correctly tell you which part is failing if one
91210 breaks.
92211
93212#### Importing in test files:
94213
95- Keep things local! prefer to import only from the file-under-test when possible.
214+ Keep things local! Prefer to import only from the file-under-test when possible.
96215This helps keep the context of the unit tests focused on the file-under-test.
97216
98217It makes refactoring much smoother; think about factoring a class out of a
@@ -143,17 +262,6 @@ def test_func():
143262- Importing from other source files is a code smell (for unit tests), It
144263 indicates that the test is not well isolated.
145264
146- It is worth cultivating a deep understanding of how python's imports work. The
147- interactions between imports and patches can some times be surprising, and cause
148- us to write invalid tests... or worse, tests that pass when they should fail.
149- These are a few of the cases that I have seen cause the most confusion.
150-
151- - If you import ` SomeThing ` from your file-under-test, Then patch
152- ` file.under.test.SomeThing ` , it does not patch ` SomeThing ` in your test file.
153- Only in the file-under-test. So, code in your file-under-test which calls
154- ` SomeThing() ` , will use the Mock. But in your test case. ` SomeThing() ` will
155- create a new instance, not call the Mock.
156-
157265- Prefer to import only the object that you actually use, not the entire
158266 library.
159267 - This simplifies mocking/patching in unit tests.
@@ -212,6 +320,48 @@ def test_myfunction(mocker):
212320 it needs fewer mocks, less setup, and fewer assertions in a single test case.
213321 This frequently leads us to write more readable and maintainable code.
214322
323+ It is worth cultivating a deep understanding of how python's imports work. The
324+ interactions between imports and patches can sometimes be surprising, and cause
325+ us to write invalid tests... or worse, tests that pass when they should fail.
326+ These are a few of the cases that cause the most confusion.
327+
328+ - When patches and imports are both used in a test case, the patch only applies
329+ to the specific context in which it is called, and does not override the
330+ import used elsewhere in the test file.
331+ - You import ` say_hello ` from your file-under-test, then patch
332+ ` src.lib.say_hello ` . If your source code calls ` say_hello ` it will use the
333+ Mock provided by the patch. But if your test case calls ` say_hello ` , it will
334+ not use the Mock, and instead will execute the function
335+ - The behavior is the same when using stdlib.mock.patch, and pytest-mocker
336+
337+ ``` python
338+ # src.lib
339+ def dangerous_sideffects ():
340+ raise RuntimeError (" BOOM" )
341+
342+
343+ def say_hello ():
344+ dangerous_sideffects()
345+ return " hello world"
346+ ```
347+
348+ ``` python
349+ from src.lib import say_hello, dangerous_sideffects
350+
351+
352+ def test_pytest (mocker ):
353+ # Given this context
354+ mock_say_hello = mocker.patch(" src.lib.dangerous_sideffects" )
355+ # When we run the code
356+ ret = say_hello()
357+ # Then we expect the result
358+ assert ret == " hello world"
359+ mock_dangerous_sideffects.assert_called_once()
360+
361+ # But this will still raise an exception!
362+ dangerous_sideffects()
363+ ```
364+
215365## Diagnostic Tests
216366
217367Diagnostic tests are used to verify the installation of a package. They should
@@ -228,8 +378,8 @@ troubleshoot problems.
228378### Guidelines for Diagnostic Tests
229379
230380- Consider using the stdlib ` unittest.TestCase ` and other stdlib tools instead
231- of pytest. To allow running unit tests for diagnostics in production environments,
232- without installing additional packages.
381+ of pytest. To allow running unit tests for diagnostics in production
382+ environments, without installing additional packages.
233383
234384- Test files should be named ` test_{{file under test}}.py ` , so that stdlib
235385 unittest can find them easily.
@@ -263,4 +413,3 @@ stdlib's unittest can be used in environments where pytest is not available:
263413- To use unittest to run tests in your source folder, from your package root,
264414 use
265415 ` python -m unittest discover --start-folder {{source folder}} --top-level-directory . `
266-
0 commit comments