Why Good Developers�Write Bad Tests
Michael Lynch
PyGotham 2019
Best practices
...for production code
Unit tests
def test_converts_farenheit_to_celsius(self):
self.assertAlmostEqual(100.0,
converter.farenheit_to_celsius(degrees=212.0),
places=1)
What’s wrong with this test?
def test_initial_score(self):
initial_score = self.account_manager.get_score(username='joe123')
self.assertEqual(150, initial_score)
The answers you seek are in setUp
def setUp(self):
database = MockDatabase()
database.add_row({
'username': 'joe123',
'score': 150
})
self.account_manager = AccountManager(database)
def test_initial_score(self):
initial_score = self.account_manager.get_score(username='joe123')
self.assertEqual(150, initial_score)
Test code is not like other code
Production code:
��Good production code:
Test code:
Good test code:
Tests are diagnostic tools
Can another developer diagnose a failure?
def test_initial_score(self):
initial_score = self.account_manager.get_score(username='joe123')
self.assertEqual(150, initial_score)
Keep the reader in your test function
def test_initial_score(self):
database = MockDatabase()
database.add_row({
'username': 'joe123',
'score': 150
})
account_manager = AccountManager(database)
initial_score = account_manager.get_score(username='joe123')
self.assertEqual(150, initial_score)
Arrange
Act
Assert
The reader should understand your test without reading any other code.
Dare to violate DRY
Dare to violate DRY
def get_usernames():� connection = sqlite3.connect(DB_PATH)� cursor = connection.cursor()� cursor.execute(� 'SELECT username FROM users')� usernames = cursor.fetchall()� connection.close()� return usernames
def get_user_ids():� connection = sqlite3.connect(DB_PATH)� cursor = connection.cursor()� cursor.execute(� 'SELECT id FROM users')� user_ids = cursor.fetchall()� connection.close()� return user_ids
def _fetchall_single_column(column):� connection = sqlite3.connect(DB_PATH)� cursor = connection.cursor()� cursor.execute(� 'SELECT %s FROM users' % column)� values = cursor.fetchall()� connection.close()� return values��def get_usernames():� return _fetchall_single_column('username')��def get_user_ids():� return _fetchall_single_column('id')
Dare to violate DRY
def test_increase_score(self):� database = MockDatabase()� database.add_row({� 'username': 'joe123',� 'score': 150� })� account_manager =� AccountManager(database)�� account_manager.adjust_score(� username='joe123', adjustment=25)
self.assertEqual(175,� account_manager.get_score(� username='joe123'))
def test_initial_score(self):� database = MockDatabase()� database.add_row({� 'username': 'joe123',� 'score': 150� })� account_manager =� AccountManager(database)�� initial_score = � account_manager.get_score(� username='joe123')
self.assertEqual(150, initial_score)
Accept redundancy if it supports simplicity.
What about when you need helper methods?
def test_increase_score(self):� user_database = MockDatabase()� user_database.add_row({� 'username': 'joe123',� 'score': 150� })� privilege_database = MockDatabase()� privilege_database.add_row({� 'privilege': 'upvote',� 'minimum_score': 200� })� privilege_manager = PrivilegeManager(privilege_database)� url_downloader = UrlDownloader()� account_manager = AccountManager(user_database, privilege_manager, url_downloader)�� account_manager.adjust_score(username='joe123', adjustment=25)
self.assertEqual(175, account_manager.get_score(username='joe123'))
Improving your production code
simplifies your test code.
Cardinal sin of test helper methods
Cardinal sin of test helper methods
def test_increase_score(self):� self.account_manager = AccountManager()� self.add_dummy_account()�� self.account_manager.adjust_score(� username='joe123',� adjustment=25)
self.assertEqual(175,� account_manager.get_score(� username='joe123'))
def add_dummy_account(self):� dummy_account = Account(� username='joe123',� name='Joe Bloggs',� email='joe123@example.com',� score=150)�� self.account_manager.add_account(� dummy_account)
Helper Method
Test Method
Using helper methods responsibly
def test_increase_score(self):� account_manager = AccountManager()� account_manager.add_account(� make_dummy_account(username='joe123',� score=150))�� account_manager.adjust_score(� username='joe123', adjustment=25)
self.assertEqual(175,� account_manager.get_score(username='joe123'))
def make_dummy_account(� self, username, score):� return Account(username=username,� name='Dummy User',� email='dummy@example.com',� score=score)
Helper Method
Test Method
Don’t bury critical information in your test helper methods.
Go crazy with long test names
Can you diagnose this failure?
class Tokenizer(object):�� def __init__(self, stream):� self._stream = stream�� def next_token(self):� ...�
======================================================================�FAIL: test_next_token (tests.test_tokenizer.TokenizerTest)�----------------------------------------------------------------------�AssertionError: '' is not None
What about this one?
======================================================================�FAIL: test_next_token_returns_None_when_stream_is_empty (tests.test_tokenizer.TokenizerTest)�----------------------------------------------------------------------�AssertionError: '' is not None
class Tokenizer(object):�� def __init__(self, stream):� self._stream = stream�� def next_token(self):� ...�
Name your tests so well that others can diagnose failures from the name alone.
Embrace magic numbers
employee_pay = calculate_pay(80)
HOURS_PER_WEEK = 40
WEEKS_PER_PAY_PERIOD = 2
employee_pay = calculate_pay(hours=HOURS_PER_WEEK * WEEKS_PER_PAY_PERIOD)
A test that avoids magic numbers
def test_greeting_appends_first_name(self):
GREETING = 'Look who’s here! It’s'
FIRST_NAME = 'Michael'
LAST_NAME = 'Lynch'
greeter = greeting.Greeter(GREETING, FIRST_NAME, LAST_NAME)
expected_message = GREETING + FIRST_NAME
self.assertEqual(expected_message, greeter.welcome_message())
A test that embraces magic numbers
def test_greeting_appends_first_name(self):
greeter = greeting.Greeter(greeting='Look who’s here! It’s',
first_name='Michael', last_name='Lynch')
self.assertEqual('Look who’s here! It’s Michael', greeter.welcome_message())
A test that avoids magic numbers
def test_greeting_appends_first_name(self):
GREETING = 'Look who’s here! It’s'
FIRST_NAME = 'Michael'
LAST_NAME = 'Lynch'
greeter = greeting.Greeter(GREETING, FIRST_NAME, LAST_NAME)
expected_message = GREETING + FIRST_NAME
self.assertEqual(expected_message, greeter.welcome_message())
GREETING + FIRST_NAME = 'Look who’s here! It’sMichael'
Why don’t we like magic numbers?
Prefer magic numbers to named constants in test code.
Summary
Thanks!
Tell me about services you wish existed