Skip to content

Commit e1115f1

Browse files
committed
add outside-in to testing principles
1 parent 0423bc6 commit e1115f1

File tree

1 file changed

+184
-35
lines changed

1 file changed

+184
-35
lines changed

docs/pages/principles/testing.md

Lines changed: 184 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -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

24143
Unit 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
26145
expected 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
29148
run them quickly, while we make changes to the code (even automatically, each
30149
time we save a file for example) to ensure our changes did not break anything...
31150
or 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
34153
better 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:
46165
Unit tests are considered "low level", and used for [Isolation Testing](). Not
47166
all 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.
96215
This helps keep the context of the unit tests focused on the file-under-test.
97216

98217
It 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

217367
Diagnostic 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

Comments
 (0)