Skip to content

Commit 64c51c7

Browse files
committed
Item 56: Test everything with unittest
1 parent edec06b commit 64c51c7

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

item_56_test_utils.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# utils_test.py
2+
from unittest import TestCase, main
3+
from item_56_utils import to_str
4+
from tempfile import TemporaryDirectory
5+
6+
7+
class UtilsTestCase(TestCase):
8+
def test_to_str_bytes(self):
9+
self.assertEqual('hello', to_str(b'hello'))
10+
11+
def test_to_str_str(self):
12+
self.assertEqual('hello', to_str('hello'))
13+
14+
def test_to_str_bad(self):
15+
self.assertRaises(TypeError, to_str, object())
16+
17+
18+
class MyTest(TestCase):
19+
def setUp(self):
20+
self.test_dir = TemporaryDirectory()
21+
22+
def tearDown(self):
23+
self.test_dir.cleanup()
24+
25+
def test_to_str_bytes(self):
26+
self.assertEqual('hello', to_str(b'hello'))
27+
28+
def test_to_str_str(self):
29+
self.assertEqual('hello', to_str('hello'))
30+
31+
def test_to_str_bad(self):
32+
self.assertRaises(TypeError, to_str, object())
33+
34+
35+
if __name__ == '__main__':
36+
main()

item_56_unittest.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Item 56: Test everything with unittest
2+
from unittest import TestCase, main
3+
from item_56_utils import to_str
4+
5+
6+
# Python doesn't have static type checking. There's nothing int eh compiler
7+
# that ensure your program will work when you run it. With Python you don't
8+
# know whether the functions your program calls will be defined at runtime,
9+
# even when their existence is evident in the source code. This dynamic
10+
# behavior is a blessing and a curse.
11+
12+
# The large numbers of Python programmers out there say it's worth it because
13+
# of the productivity gained from the resulting brevity and simplicity. But
14+
# most people have heard at least one horror story about Python in which a
15+
# program encountered a boneheaded error at run time.
16+
17+
# One of the worst examples I've heard is when a SyntaxError was raised in
18+
# production as a side effect of a dynamic import (see Item 52: "Know how to
19+
# break circular dependencies"). The programmer I know who was hit by this
20+
# surprising occurrence has since ruled out using Python ever again.
21+
22+
# But I have to wonder, why wasn't the code tested before the program was
23+
# deployed to production? Type safety isn't everything. You should always test
24+
# your code, regardless of what language it's written in. However, I'll admit
25+
# that the big difference between Python and many other languages is that the
26+
# only way to have any confidence in a Python program is by writing tests.
27+
# There is no veil of static type checking to make you feel safe.
28+
29+
# Luckily, the same dynamic features that prevent static type checking in
30+
# Python also make it extremely easy to write test for your code. You can use
31+
# Python's dynamic nature and easily overridable behaviors to implement tests
32+
# and ensure that your programs work as expected.
33+
34+
# You should think of tests as an insurance policy on your code. Good tests
35+
# give you confidence that your code is correct. If you refactor or expand
36+
# your code, tests make it easy to identify how behaviors have changed. It
37+
# sounds counter-intuitive, but having good tests actually makes it easier to
38+
# modify Python code, not harder.
39+
40+
# The simplest way to write tests is to use the unittest built-in module. For
41+
# example, say you have the following utility function defined in utils.py
42+
43+
44+
def to_str(data):
45+
if isinstance(data, str):
46+
return data
47+
elif isinstance(data, bytes):
48+
return data.decode('utf-8')
49+
else:
50+
raise TypeError('Must supply str or bytes, '
51+
'found: %r' % data)
52+
53+
54+
# To define tests, I create a second file named test_utils.py or utils_test.py
55+
56+
57+
class UtilsTestCase(TestCase):
58+
def test_to_str_bytes(self):
59+
self.assertEqual('hello', to_str(b'hello'))
60+
61+
def test_to_str_str(self):
62+
self.assertEqual('hello', to_str('hello'))
63+
64+
def test_to_str_bad(self):
65+
self.assertRaises(TypeError, to_str, object())
66+
67+
68+
if __name__ == '__main__':
69+
main()
70+
71+
72+
# Tests are organized into TestCase classes. Each test is a method beginning
73+
# with the word test. If a test method runs without raising any kind of
74+
# Exception (including AssertionError from assert statements), then the test
75+
# is considered to have passed successfully.
76+
77+
# The TestCase class provides helper methods for making assertions in your
78+
# tests, such as assertEqual for verifying equality, assertTrue for verifying
79+
# that exceptions are raised when appropriate (see help (TestCase) for more).
80+
# You can define your own helper methods in TestCase subclasses to make your
81+
# tests more readable; just ensure that your method names don't begin with
82+
# the word test.
83+
84+
# Note
85+
# Another common practice when writing test is to use mock functions and
86+
# classes to stub out certain behaviors. For this purpose, Python 3 provides
87+
# the unittest.mock built-in module, which is available for Python 2 as an
88+
# open source package.
89+
90+
# Sometimes, your TestCase classes need to set up the test environment before
91+
# running test methods. To do this, you can override the setUp and tearDown
92+
# methods. These methods are called before and after each test method,
93+
# respectively, and they let you ensure that each test runs in isolation (an
94+
# important best practice of proper testing). For example, here I define a
95+
# TestCase that creates a temporary directory before each test and deletes its
96+
# contents after each test finishes:
97+
98+
99+
class MyTest(TestCase):
100+
def setUp(self):
101+
self.test_dir = TemporaryDirectory()
102+
103+
def tearDown(self):
104+
self.test_dir.cleanup()
105+
#
106+
# def test_to_str_bytes(self):
107+
# self.assertEqual('hello', to_str(b'hello'))
108+
#
109+
# def test_to_str_str(self):
110+
# self.assertEqual('hello', to_str('hello'))
111+
#
112+
# def test_to_str_bad(self):
113+
# self.assertRaises(TypeError, to_str, object())
114+
115+
# I usually define one TestCase for each set of related tests. Sometimes I
116+
# have one TestCase for each function that has many edge cases. Other times,
117+
# a TestCase spans all functions in a single module. I'll also create one
118+
# TestCase for testing a single class and all of its methods.
119+
120+
# When programs get complicated, you'll want additional test for verifying
121+
# the interactions between your modules, instead of only testing code in
122+
# isolation. This is the difference between unit tests and integration tests.
123+
# In Python, it's important to write both types of test for exactly the same
124+
# reason: You have no guarantee that your modules will actually work together
125+
# unless you prove it.
126+
127+
# Note
128+
# Depending on your project, it can also be useful to define data-driven tests
129+
# or organize test into different suites of related functionality. For these
130+
# purpose, code coverage reports, and other advanced use cases, the nose
131+
# (http://nose.readthedocs.org/) and pytest (http://pytest.org/) open source
132+
# packages can be especially helpful.
133+
134+
135+
# Things to remember
136+
137+
# 1. The only way to have confidence in a Python program is to write tests.
138+
# 2. The unittest built-in module provides most of the facilities you'll need
139+
# to write good tests.
140+
# 3. You can define tests by subclassing TestCase and defining one method per
141+
# behavior you'd like to test. Test methods on TestCase classes must start
142+
# with the word test.
143+
# 4. It's important to write both unit tests (for isolated functionality) and
144+
# integration tests (for modules that interact).

item_56_utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# utils.py
2+
3+
4+
def to_str(data):
5+
if isinstance(data, str):
6+
return data
7+
elif isinstance(data, bytes):
8+
return data.decode('utf-8')
9+
else:
10+
raise TypeError('Must supply str or bytes, '
11+
'found: %r' % data)

0 commit comments

Comments
 (0)