Testing in Hard to Reach Places




            Lee Begg
           Begg Digital
Overview
●   Goals
●   The module
●   Refactoring
●   SqlAlchemy tricks
●   Mocking hard things


      Code: http://github.com/llnz/kiwipycon2011testing
      Slides: http://www.beggdigital.com/media/kiwipycon2011testing.pdf
Goals
●   Make sure it works properly
    ●   All the time
    ●   In all conditions
●   Unit testing to check
    ●   Cover 100%
    ●   Might still miss cases
Example module
  def main():
    '''Check the tripwire, email if the latest values have an RMS value over 2'''

    session = model.Session()
    values = session.query(model.MeasuredValue).order_by(model.MeasuredValue.desc()).limit(20).all()

    totalsq = 0
    for value in values:
        totalsq += value.value**2
    rms = math.sqrt(totalsq)
    if rms > 2:
        body = """Database trip wire has been tripped

RMS value was: %s
"""

    subject = "Tripwire report %s" % datetime.date.today()

    msg = MIMEText(body)
    msg['Subject'] = '[DBTW] %s' % subject
    msg['To'] = ','.join(settings.TO_ADDRESSES)
    msg['From'] = settings.FROM_ADDRESS

    server = smtplib.SMTP('localhost')
    result = server.sendmail(settings.FROM_ADDRESS, settings.TO_ADDRESSES, msg.as_string())
    if len(result) != 0:
        print "sendmail failures: %s" % result
    server.quit()
Refactoring
●   Refactor out components to test
class RMSTest(unittest.TestCase):
   '''Test the RMS calculations'''

    def testRMSCalc(self):                   def calcRMS(values):
      testvalues = [                           totalsq = 0.0
               ([0], 0),                       for value in values:
               ([1], 1),                          totalsq += value**2
               ([2], 2),
               ([0, 0], 0),                    rms = math.sqrt(totalsq/len(values))
               ([1, 1], 1),                    return rms
               ([0] * 20, 0),
               ([1] * 20, 1),
               ([0, 0, 0, 1], 0.5),
               ([3, 1, 3, 0, 1], 2),
               ]

    for values, expected in testvalues:
       result = tripwire.calcRMS(values)
       self.assertAlmostEqual(result, expected, msg='rmsCalc(%s) gave %s,
expected %s' % (values, result, expected))
SqlAlchemy Tricks
●   Change the database in the unittest setUp
    method
●   Done internally by Django


#override database setting
from dbtripwire import settings
settings.DATABASE_URL = 'sqlite:///:memory:'


from dbtripwire.model import initDatabase, dropDatabase
SqlAlchemy Tricks
class DatabaseTestSetup(object):
   '''Create the database in the setUp, and drop it in tearDown

  Used to abstract this away from all the unittests that use the Database.

  Must be the first class inherited from, or TestCase will override these
  methods, not the other way around.
  '''

  def setUp(self):
    '''Initialise the database with the tables'''
    initDatabase()


  def tearDown(self):
    '''Drop the tables'''
    dropDatabase()
SqlAlchemy Tricks
class ModelTest(DatabaseTestSetup, unittest.TestCase):
   '''Test the model classes'''

  def testMeasuredValueTable(self):
    '''MeasuredValue table test'''

    session = model.Session()

    self.assertEqual(session.query(model.MeasuredValue).count(), 0)

    mv = model.MeasuredValue(5)
    self.assert_(mv)

    session.add(mv)
    session.commit()

    self.assertEqual(session.query(model.MeasuredValue).count(), 1)

    mv1 = session.query(model.MeasuredValue).one()
    self.assertEqual(mv1.id, 1)
    self.assertEqual(mv1.value, 5)
    #don't forget to test the __repr__ string
    self.assertEqual(repr(mv1), "<MeasuredValue(1, 5)>")

    session.delete(mv1)

    session.commit()

    self.assertEqual(session.query(model.MeasuredValue).count(), 0)
Mock
●   Creating fake “mock” objects/classes/modules
●   Replace things you couldn't normally control
●   A few frameworks available
●   Very useful in replacing network connections
    ●   Httplib for example
    ●   Smtplib for another
Mock example
class SendEmailTest(unittest.TestCase):
   '''Test sending email'''

  def testSendEmail(self):
    tt = minimock.TraceTracker()
    smtpconn = minimock.Mock('smtplib.SMTP', tracker=tt)
    minimock.mock('smtplib.SMTP', mock_obj=smtpconn)
    smtpconn.mock_returns = smtpconn
    smtpconn.sendmail.mock_returns = {}

       tripwire.sendEmail(2.5, datetime.date(2011, 8, 16))
       expected = r"""Called smtplib.SMTP('localhost')
Called smtplib.SMTP.sendmail(
   'lee@beggdigital.co.nz',
   ['lee@beggdigital.co.nz', 'llnz@paradise.net.nz'],
   'Content-Type: text/plain; charset="us-ascii"nMIME-Version: 1.0nContent-Transfer-Encoding:
7bitnSubject: [DBTW] Tripwire Report 2011-08-16nTo: lee@beggdigital.co.nz,
llnz@paradise.net.nznFrom: lee@beggdigital.co.nznnDatabase trip wire has been tripped.nnRMS
value was: 2.5n')
Called smtplib.SMTP.quit()"""
       self.assertTrue(tt.check(expected), tt.diff(expected))

    minimock.restore()

Testing in those hard to reach places

  • 1.
    Testing in Hardto Reach Places Lee Begg Begg Digital
  • 2.
    Overview ● Goals ● The module ● Refactoring ● SqlAlchemy tricks ● Mocking hard things Code: http://github.com/llnz/kiwipycon2011testing Slides: http://www.beggdigital.com/media/kiwipycon2011testing.pdf
  • 3.
    Goals ● Make sure it works properly ● All the time ● In all conditions ● Unit testing to check ● Cover 100% ● Might still miss cases
  • 4.
    Example module def main(): '''Check the tripwire, email if the latest values have an RMS value over 2''' session = model.Session() values = session.query(model.MeasuredValue).order_by(model.MeasuredValue.desc()).limit(20).all() totalsq = 0 for value in values: totalsq += value.value**2 rms = math.sqrt(totalsq) if rms > 2: body = """Database trip wire has been tripped RMS value was: %s """ subject = "Tripwire report %s" % datetime.date.today() msg = MIMEText(body) msg['Subject'] = '[DBTW] %s' % subject msg['To'] = ','.join(settings.TO_ADDRESSES) msg['From'] = settings.FROM_ADDRESS server = smtplib.SMTP('localhost') result = server.sendmail(settings.FROM_ADDRESS, settings.TO_ADDRESSES, msg.as_string()) if len(result) != 0: print "sendmail failures: %s" % result server.quit()
  • 5.
    Refactoring ● Refactor out components to test class RMSTest(unittest.TestCase): '''Test the RMS calculations''' def testRMSCalc(self): def calcRMS(values): testvalues = [ totalsq = 0.0 ([0], 0), for value in values: ([1], 1), totalsq += value**2 ([2], 2), ([0, 0], 0), rms = math.sqrt(totalsq/len(values)) ([1, 1], 1), return rms ([0] * 20, 0), ([1] * 20, 1), ([0, 0, 0, 1], 0.5), ([3, 1, 3, 0, 1], 2), ] for values, expected in testvalues: result = tripwire.calcRMS(values) self.assertAlmostEqual(result, expected, msg='rmsCalc(%s) gave %s, expected %s' % (values, result, expected))
  • 6.
    SqlAlchemy Tricks ● Change the database in the unittest setUp method ● Done internally by Django #override database setting from dbtripwire import settings settings.DATABASE_URL = 'sqlite:///:memory:' from dbtripwire.model import initDatabase, dropDatabase
  • 7.
    SqlAlchemy Tricks class DatabaseTestSetup(object): '''Create the database in the setUp, and drop it in tearDown Used to abstract this away from all the unittests that use the Database. Must be the first class inherited from, or TestCase will override these methods, not the other way around. ''' def setUp(self): '''Initialise the database with the tables''' initDatabase() def tearDown(self): '''Drop the tables''' dropDatabase()
  • 8.
    SqlAlchemy Tricks class ModelTest(DatabaseTestSetup,unittest.TestCase): '''Test the model classes''' def testMeasuredValueTable(self): '''MeasuredValue table test''' session = model.Session() self.assertEqual(session.query(model.MeasuredValue).count(), 0) mv = model.MeasuredValue(5) self.assert_(mv) session.add(mv) session.commit() self.assertEqual(session.query(model.MeasuredValue).count(), 1) mv1 = session.query(model.MeasuredValue).one() self.assertEqual(mv1.id, 1) self.assertEqual(mv1.value, 5) #don't forget to test the __repr__ string self.assertEqual(repr(mv1), "<MeasuredValue(1, 5)>") session.delete(mv1) session.commit() self.assertEqual(session.query(model.MeasuredValue).count(), 0)
  • 9.
    Mock ● Creating fake “mock” objects/classes/modules ● Replace things you couldn't normally control ● A few frameworks available ● Very useful in replacing network connections ● Httplib for example ● Smtplib for another
  • 10.
    Mock example class SendEmailTest(unittest.TestCase): '''Test sending email''' def testSendEmail(self): tt = minimock.TraceTracker() smtpconn = minimock.Mock('smtplib.SMTP', tracker=tt) minimock.mock('smtplib.SMTP', mock_obj=smtpconn) smtpconn.mock_returns = smtpconn smtpconn.sendmail.mock_returns = {} tripwire.sendEmail(2.5, datetime.date(2011, 8, 16)) expected = r"""Called smtplib.SMTP('localhost') Called smtplib.SMTP.sendmail( 'lee@beggdigital.co.nz', ['lee@beggdigital.co.nz', 'llnz@paradise.net.nz'], 'Content-Type: text/plain; charset="us-ascii"nMIME-Version: 1.0nContent-Transfer-Encoding: 7bitnSubject: [DBTW] Tripwire Report 2011-08-16nTo: lee@beggdigital.co.nz, llnz@paradise.net.nznFrom: lee@beggdigital.co.nznnDatabase trip wire has been tripped.nnRMS value was: 2.5n') Called smtplib.SMTP.quit()""" self.assertTrue(tt.check(expected), tt.diff(expected)) minimock.restore()