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