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.