Most developers' #1 thought about testing:
I AM BAD
A simple system under test:
01 | # portfolio1.py |
02 |
03 | class Portfolio( object ): |
04 | """A simple stock portfolio""" |
05 | def __init__( self ): |
06 | # stocks is a list of lists: |
07 | # [[name, shares, price], ...] |
08 | self .stocks = [] |
09 |
10 | def buy( self , name, shares, price): |
11 | """Buy `name`: `shares` shares at `price`.""" |
12 | self .stocks.append([name, shares, price]) |
13 |
14 | def cost( self ): |
15 | """What was the total cost of this portfolio?""" |
16 | amt = 0.0 |
17 | for name, shares, price in self .stocks: |
18 | amt + = shares * price |
19 | return amt |
01 | $ python |
02 | Python 2.7 . 2 (default, Jun 12 2011 , 15 : 08 : 59 ) |
03 | >>> from portfolio1 import Portfolio |
04 | >>> p = Portfolio() |
05 | >>> p.cost() |
06 | 0.0 |
07 |
08 | >>> p.buy( "IBM" , 100 , 176.48 ) |
09 | >>> p.cost() |
10 | 17648.0 |
11 |
12 | >>> p.buy( "HPQ" , 100 , 36.15 ) |
13 | >>> p.cost() |
14 | 21263.0 |
1 | # porttest1.py |
2 | from portfolio1 import Portfolio |
3 |
4 | p = Portfolio() |
5 | print "Empty portfolio cost: %s" % p.cost() |
6 | p.buy( "IBM" , 100 , 176.48 ) |
7 | print "With 100 IBM @ 176.48: %s" % p.cost() |
8 | p.buy( "HPQ" , 100 , 36.15 ) |
9 | print "With 100 HPQ @ 36.15: %s" % p.cost() |
1 | $ python porttest1.py |
2 | Empty portfolio cost: 0.0 |
3 | With 100 IBM @ 176.48: 17648.0 |
4 | With 100 HPQ @ 36.15: 21263.0 |
1 | # porttest2.py |
2 | from portfolio1 import Portfolio |
3 |
4 | p = Portfolio() |
5 | print "Empty portfolio cost: %s, should be 0.0" % p.cost() |
6 | p.buy( "IBM" , 100 , 176.48 ) |
7 | print "With 100 IBM @ 176.48: %s, should be 17648.0" % p.cost() |
8 | p.buy( "HPQ" , 100 , 36.15 ) |
9 | print "With 100 HPQ @ 36.15: %s, should be 21263.0" % p.cost() |
1 | $ python porttest2.py |
2 | Empty portfolio cost: 0.0, should be 0.0 |
3 | With 100 IBM @ 176.48: 17648.0, should be 17648.0 |
4 | With 100 HPQ @ 36.15: 21263.0, should be 21263.0 |
01 | # porttest3.py |
02 | from portfolio1 import Portfolio |
03 |
04 | p = Portfolio() |
05 | print "Empty portfolio cost: %s, should be 0.0" % p.cost() |
06 | assert p.cost() = = 0.0 |
07 | p.buy( "IBM" , 100 , 176.48 ) |
08 | print "With 100 IBM @ 176.48: %s, should be 17648.0" % p.cost() |
09 | assert p.cost() = = 17648.0 |
10 | p.buy( "HPQ" , 100 , 36.15 ) |
11 | print "With 100 HPQ @ 36.15: %s, should be 21263.0" % p.cost() |
12 | assert p.cost() = = 21263.0 |
1 | $ python porttest3.py |
2 | Empty portfolio cost: 0.0, should be 0.0 |
3 | With 100 IBM @ 176.48: 17648.0, should be 17648.0 |
4 | With 100 HPQ @ 36.15: 21263.0, should be 21263.0 |
1 | $ python porttest3_broken.py |
2 | Empty portfolio cost: 0.0, should be 0.0 |
3 | With 100 IBM @ 176.48: 17600.0, should be 17648.0 |
4 | Traceback (most recent call last): |
5 | File "porttest3_broken.py", line 9, in <module> |
6 | assert p.cost() == 17648.0 |
7 | AssertionError |
01 | # test_port1.py |
02 |
03 | import unittest |
04 | from portfolio1 import Portfolio |
05 |
06 | class PortfolioTest(unittest.TestCase): |
07 | def test_ibm( self ): |
08 | p = Portfolio() |
09 | p.buy( "IBM" , 100 , 176.48 ) |
10 | assert p.cost() = = 17648.0 |
11 |
12 | if __name__ = = '__main__' : |
13 | unittest.main() |
1 | $ python test_port1.py |
2 | . |
3 | ---------------------------------------------------------------------- |
4 | Ran 1 test in 0.000s |
5 |
6 | OK |
1 | # unittest runs the tests as if I had written: |
2 | testcase = PortfolioTest() |
3 | try : |
4 | testcase.test_ibm() |
5 | except : |
6 | [record failure] |
7 | else : |
8 | [record success] |
06 | class PortfolioTest(unittest.TestCase): |
07 | def test_empty( self ): |
08 | p = Portfolio() |
09 | assert p.cost() = = 0.0 |
10 |
11 | def test_ibm( self ): |
12 | p = Portfolio() |
13 | p.buy( "IBM" , 100 , 176.48 ) |
14 | assert p.cost() = = 17648.0 |
15 |
16 | def test_ibm_hpq( self ): |
17 | p = Portfolio() |
18 | p.buy( "IBM" , 100 , 176.48 ) |
19 | p.buy( "HPQ" , 100 , 36.15 ) |
20 | assert p.cost() = = 21263.0 |
1 | $ python test_port2.py |
2 | ... |
3 | ---------------------------------------------------------------------- |
4 | Ran 3 tests in 0.000s |
5 |
6 | OK |
01 | # unittest runs the tests as if I had written: |
02 | testcase = PortfolioTest() |
03 | try : |
04 | testcase.test_empty() |
05 | except : |
06 | [record failure] |
07 | else : |
08 | [record success] |
09 |
10 | testcase = PortfolioTest() |
11 | try : |
12 | testcase.test_ibm() |
13 | except : |
14 | [record failure] |
15 | else : |
16 | [record success] |
17 |
18 | testcase = PortfolioTest() |
19 | try : |
20 | testcase.test_ibm_hpq() |
21 | except : |
22 | [record failure] |
23 | else : |
24 | [record success] |
01 | $ python test_port2_broken.py |
02 | .F. |
03 | ====================================================================== |
04 | FAIL: test_ibm (__main__.PortfolioTest) |
05 | ---------------------------------------------------------------------- |
06 | Traceback (most recent call last): |
07 | File "test_port2_broken.py", line 14, in test_ibm |
08 | assert p.cost() == 17648.0 |
09 | AssertionError |
10 |
11 | ---------------------------------------------------------------------- |
12 | Ran 3 tests in 0.001s |
13 |
14 | FAILED (failures=1) |
11 | def test_ibm( self ): |
12 | p = Portfolio() |
13 | p.buy( "IBM" , 100 , 176.48 ) |
14 | self .assertEqual(p.cost(), 17648.0 ) |
01 | $ python test_port3_broken.py |
02 | .F. |
03 | ====================================================================== |
04 | FAIL: test_ibm (__main__.PortfolioTest) |
05 | ---------------------------------------------------------------------- |
06 | Traceback (most recent call last): |
07 | File "test_port3_broken.py", line 14, in test_ibm |
08 | self.assertEqual(p.cost(), 17648.0) |
09 | AssertionError: 17600.0 != 17648.0 |
10 |
11 | ---------------------------------------------------------------------- |
12 | Ran 3 tests in 0.001s |
13 |
14 | FAILED (failures=1) |
01 | assertEqual(first, second) |
02 | assertNotEqual(first, second) |
03 | assertTrue(expr) |
04 | assertFalse(expr) |
05 | assertIn(first, second) |
06 | assertNotIn(first, second) |
07 | assertAlmostEqual(first, second) |
08 | assertGreater(first, second) |
09 | assertLess(first, second) |
10 | assertRegexMatches(text, regexp) |
11 | .. etc .. |
01 | $ python test_port3_broken2.py |
02 | .E. |
03 | ====================================================================== |
04 | ERROR: test_ibm (__main__.PortfolioTest) |
05 | ---------------------------------------------------------------------- |
06 | Traceback (most recent call last): |
07 | File "test_port3_broken2.py", line 13, in test_ibm |
08 | p.buyxxxxx("IBM", 100, 176.48) |
09 | AttributeError: 'Portfolio' object has no attribute 'buyxxxxx' |
10 |
11 | ---------------------------------------------------------------------- |
12 | Ran 3 tests in 0.000s |
13 |
14 | FAILED (errors=1) |
1 | testcase = PortfolioTest() |
2 | try : |
3 | testcase.test_method() |
4 | except AssertionError: |
5 | [record failure] |
6 | except : |
7 | [record error] |
8 | else : |
9 | [record success] |
1 | $ python |
2 | Python 2.7 . 2 (default, Jun 12 2011 , 15 : 08 : 59 ) |
3 | >>> from portfolio1 import Portfolio |
4 | >>> p = Portfolio() |
5 | >>> p.buy( "IBM" ) |
6 | Traceback (most recent call last): |
7 | File "<console>" , line 1 , in <module> |
8 | TypeError: buy() takes exactly 4 arguments ( 2 given) |
22 | def test_bad_input( self ): |
23 | p = Portfolio() |
24 | p.buy( "IBM" ) |
01 | $ python test_port4_broken.py |
02 | E... |
03 | ====================================================================== |
04 | ERROR: test_bad_input (__main__.PortfolioTest) |
05 | ---------------------------------------------------------------------- |
06 | Traceback (most recent call last): |
07 | File "test_port4_broken.py", line 24, in test_bad_input |
08 | p.buy("IBM") |
09 | TypeError: buy() takes exactly 4 arguments (2 given) |
10 |
11 | ---------------------------------------------------------------------- |
12 | Ran 4 tests in 0.001s |
13 |
14 | FAILED (errors=1) |
22 | def test_bad_input( self ): |
23 | p = Portfolio() |
24 | self .assertRaises(TypeError, p.buy, "IBM" ) |
1 | $ python test_port4.py |
2 | .... |
3 | ---------------------------------------------------------------------- |
4 | Ran 4 tests in 0.000s |
5 |
6 | OK |
Nicer in 2.7:
1 | def test_bad_input( self ): |
2 | p = Portfolio() |
3 | with self .assertRaises(TypeError): |
4 | p.buy( "IBM" ) |
21 | def sell( self , name, shares): |
22 | """Sell some number of shares of `name`.""" |
23 | for holding in self .stocks: |
24 | if holding[ 0 ] = = name: |
25 | if holding[ 1 ] < shares: |
26 | raise ValueError( "Not enough shares" ) |
27 | holding[ 1 ] - = shares |
28 | break |
29 | else : |
30 | raise ValueError( "You don't own that stock" ) |
26 | class PortfolioSellTest(unittest.TestCase): |
27 | def test_sell( self ): |
28 | p = Portfolio() |
29 | p.buy( "MSFT" , 100 , 27.0 ) |
30 | p.buy( "DELL" , 100 , 17.0 ) |
31 | p.buy( "ORCL" , 100 , 34.0 ) |
32 | p.sell( "MSFT" , 50 ) |
33 | self .assertEqual(p.cost(), 6450 ) |
34 |
35 | def test_not_enough( self ): |
36 | p = Portfolio() |
37 | p.buy( "MSFT" , 100 , 27.0 ) |
38 | p.buy( "DELL" , 100 , 17.0 ) |
39 | p.buy( "ORCL" , 100 , 34.0 ) |
40 | with self .assertRaises(ValueError): |
41 | p.sell( "MSFT" , 200 ) |
42 |
43 | def test_dont_own_it( self ): |
44 | p = Portfolio() |
45 | p.buy( "MSFT" , 100 , 27.0 ) |
46 | p.buy( "DELL" , 100 , 17.0 ) |
47 | p.buy( "ORCL" , 100 , 34.0 ) |
48 | with self .assertRaises(ValueError): |
49 | p.sell( "IBM" , 1 ) |
26 | class PortfolioSellTest(unittest.TestCase): |
27 | def setUp( self ): |
28 | self .p = Portfolio() |
29 | self .p.buy( "MSFT" , 100 , 27.0 ) |
30 | self .p.buy( "DELL" , 100 , 17.0 ) |
31 | self .p.buy( "ORCL" , 100 , 34.0 ) |
32 |
33 | def test_sell( self ): |
34 | self .p.sell( "MSFT" , 50 ) |
35 | self .assertEqual( self .p.cost(), 6450 ) |
36 |
37 | def test_not_enough( self ): |
38 | with self .assertRaises(ValueError): |
39 | self .p.sell( "MSFT" , 200 ) |
40 |
41 | def test_dont_own_it( self ): |
42 | with self .assertRaises(ValueError): |
43 | self .p.sell( "IBM" , 1 ) |
01 | testcase = PortfolioTest() |
02 | try : |
03 | testcase.setUp() |
04 | except : |
05 | [record error] |
06 | else : |
07 | try : |
08 | testcase.test_method() |
09 | except AssertionError: |
10 | [record failure] |
11 | except : |
12 | [record error] |
13 | else : |
14 | [record success] |
15 | finally : |
16 | try : |
17 | testcase.tearDown() |
18 | except : |
19 | [record error] |
1 | class MyBadTestCase(unittest.TestCase): |
2 | def test_a_thing( self ): |
3 | old_global = some_global_thing |
4 | some_global_thing = new_test_value |
5 |
6 | do_my_test_stuff() |
7 | |
8 | some_global_thing = old_global |
01 | class MyGoodTestCase(unittest.TestCase): |
02 | def setUp( self ): |
03 | self .old_global = some_global_thing |
04 | some_global_thing = new_test_value |
05 |
06 | def tearDown( self ): |
07 | some_global_thing = self .old_global |
08 |
09 | def test_a_thing( self ): |
10 | do_my_test_stuff() |
1 | def test_with_special_settings( self ): |
2 | with patch_settings(SOMETHING = 'special' , ANOTHER = 'weird' ): |
3 | do_my_test_stuff() |
02 | from somewhere import MY_GLOBALS |
03 |
04 | NO_SUCH_SETTING = object () |
05 |
06 | @contextlib .contextmanager |
07 | def patch_settings( * * kwargs): |
08 | old_settings = [] |
09 | for key, new_value in kwargs.items(): |
10 | old_value = getattr (MY_GLOBALS, key, NO_SUCH_SETTING) |
11 | old_settings.append((key, old_value)) |
12 | setattr (MY_GLOBALS, key, new_value) |
13 |
14 | yield |
15 |
16 | for key, old_value in old_settings: |
17 | if old_value is NO_SUCH_SETTING: |
18 | delattr (MY_GLOBALS, key) |
19 | else : |
20 | setattr (MY_GLOBALS, key, old_value) |
51 | def current_prices( self ): |
52 | """Return a dict mapping names to current prices.""" |
54 | # returns comma-separated values: |
55 | # "IBM",174.23 |
56 | # "HPQ",35.13 |
58 | names = [name for name, shares, price in self .stocks] |
59 | url + = "," .join( sorted (names)) |
60 | data = urllib.urlopen(url) |
61 | prices = dict ((sym, float (last)) for sym, last in csv.reader(data)) |
62 | return prices |
63 |
64 | def value( self ): |
65 | """Return the current value of the portfolio.""" |
66 | prices = self .current_prices() |
67 | total = 0.0 |
68 | for name, shares, price in self .stocks: |
69 | total + = shares * prices[name] |
70 | return total |
01 | $ python |
02 | Python 2.7 . 2 (default, Jun 12 2011 , 15 : 08 : 59 ) |
03 | >>> from portfolio3 import Portfolio |
04 | >>> p = Portfolio() |
05 | >>> p.buy( "IBM" , 100 , 150.0 ) |
06 | >>> p.buy( "HPQ" , 100 , 30.0 ) |
07 |
08 | >>> p.current_prices() |
09 | { 'HPQ' : 35.61 , 'IBM' : 185.21 } |
10 |
11 | >>> p.value() |
12 | 22082.0 |
45 | # Replace Portfolio.current_prices with a stub implementation. |
46 | # This avoids the web, but also skips all our current_prices |
47 | # code. |
48 | class PortfolioValueTest(unittest.TestCase): |
49 | def fake_current_prices( self ): |
50 | return { 'IBM' : 140.0 , 'HPQ' : 32.0 } |
51 |
52 | def setUp( self ): |
53 | self .p = Portfolio() |
54 | self .p.buy( "IBM" , 100 , 120.0 ) |
55 | self .p.buy( "HPQ" , 100 , 30.0 ) |
56 | self .p.current_prices = self .fake_current_prices |
57 |
58 | def test_value( self ): |
59 | self .assertEqual( self .p.value(), 17200 ) |
01 | $ coverage run test_port7.py |
02 | ........ |
03 | ---------------------------------------------------------------------- |
04 | Ran 8 tests in 0.000s |
05 |
06 | OK |
07 | $ coverage report -m |
08 | Name Stmts Miss Cover Missing |
09 | ------------------------------------------ |
10 | portfolio3 33 6 82% 57-62 |
11 | test_port7 45 0 100% |
12 | ------------------------------------------ |
13 | TOTAL 78 6 92% |
51 | def current_prices( self ): |
52 | """Return a dict mapping names to current prices.""" |
54 | # returns comma-separated values: |
55 | # "IBM",174.23 |
56 | # "HPQ",35.13 |
58 | names = [name for name, shares, price in self .stocks] |
59 | url + = "," .join( sorted (names)) |
60 | data = urllib.urlopen(url) |
61 | prices = dict ((sym, float (last)) for sym, last in csv.reader(data)) |
62 | return prices |
48 | # A simple fake for urllib that implements only one method, |
49 | # and is only good for one request. You can make this much |
50 | # more complex for your own needs. |
51 | class FakeUrllib( object ): |
52 | """An object that can stand in for the urllib module.""" |
53 |
54 | def urlopen( self , url): |
55 | """A stub urllib.urlopen() implementation.""" |
56 | return StringIO( '"IBM",140\n"HPQ",32\n' ) |
57 |
58 |
59 | class PortfolioValueTest(unittest.TestCase): |
60 | def setUp( self ): |
61 | self .old_urllib = portfolio3.urllib |
62 | portfolio3.urllib = FakeUrllib() |
63 |
64 | self .p = Portfolio() |
65 | self .p.buy( "IBM" , 100 , 120.0 ) |
66 | self .p.buy( "HPQ" , 100 , 30.0 ) |
67 |
68 | def tearDown( self ): |
69 | portfolio3.urllib = self .old_urllib |
70 |
71 | def test_value( self ): |
72 | self .assertEqual( self .p.value(), 17200 ) |
01 | $ coverage run test_port8.py |
02 | ........ |
03 | ---------------------------------------------------------------------- |
04 | Ran 8 tests in 0.001s |
05 |
06 | OK |
07 | $ coverage report -m |
08 | Name Stmts Miss Cover Missing |
09 | ------------------------------------------ |
10 | portfolio3 33 0 100% |
11 | test_port8 51 0 100% |
12 | ------------------------------------------ |
13 | TOTAL 84 0 100% |
49 | class PortfolioValueTest(unittest.TestCase): |
50 | def setUp( self ): |
51 | self .p = Portfolio() |
52 | self .p.buy( "IBM" , 100 , 120.0 ) |
53 | self .p.buy( "HPQ" , 100 , 30.0 ) |
54 |
55 | def test_value( self ): |
56 | # Create a mock urllib.urlopen |
57 | with mock.patch( 'urllib.urlopen' ) as urlopen: |
58 |
59 | # When called, it will return this value |
60 | urlopen.return_value = StringIO( '"IBM",140\n"HPQ",32\n' ) |
61 |
62 | # Run the test! |
63 | self .assertEqual( self .p.value(), 17200 ) |
64 |
65 | # We can ask the mock what its arguments were |
66 | urlopen.assert_called_with( |
68 | "?f=sl1&s=HPQ,IBM" |
69 | ) |
Made with Cog, Slippy, and Fontin.