Do my unit tests follow best practices?

I've watched quite a few videos and read a couple of articles on unit testing and I've tried my best to make this test case as good as possible. In which areas can it still be improved?

I should probably add that SM2Scheduler is an implementation of the SM2 algorithm, which is used in flashcard software such as Anki and Mnemosyne.

import datetime
import os
import pathlib
import sys
import unittest

# Resolve the relative import error
path_to_this_file = pathlib.Path(__file__)
path_to_srsalgorithms = path_to_this_file.parents[1]
sys.path.append(str(path_to_srsalgorithms))

from sm2 import SM2Scheduler

class TestSM2Scheduler(unittest.TestCase):
def setUp(self):
self.scheduler = SM2Scheduler()

def test_init_last_review_date_is_today(self):
last_review_date = self.scheduler.last_review_date
today = datetime.date.today()
self.assertEqual(last_review_date, today)

def test_init_new_card_is_to_be_seen_today(self):
days_until_next_review = self.scheduler.days_until_next_review
self.assertEqual(days_until_next_review, 0)

def test_init_new_card_has_not_been_reviewed_yet(self):
number_of_repetitions = self.scheduler.number_of_repetitions
self.assertEqual(number_of_repetitions, 0)

def test_init_default_easiness_factor_is_2_point_5(self):
easiness_factor = self.scheduler.easiness_factor
self.assertEqual(easiness_factor, 2.5)

def test_feed_last_review_date_is_updated_after_repetition(self):
self.scheduler.feed(5)
last_review_date = self.scheduler.last_review_date
today = datetime.date.today()
self.assertEqual(last_review_date, today)

def test_feed_number_of_repetitions_is_set_to_0_if_card_rating_is_below_3(self):
self.scheduler.feed(2)
number_of_repetitions = self.scheduler.number_of_repetitions
self.assertEqual(number_of_repetitions, 0)

def test_feed_number_of_repetitions_is_incremented_if_card_rating_is_3_or_more(self):
self.scheduler.feed(3)
number_of_repetitions = self.scheduler.number_of_repetitions
self.assertEqual(number_of_repetitions, 1)

def test_get_date_due_for_review_returns_correct_date(self):
self.scheduler.days_until_next_review = 5
next_review_date = self.scheduler.get_date_due_for_review()
in_5_days = datetime.date.today() + datetime.timedelta(days=5)
self.assertEqual(next_review_date, in_5_days)

def test_get_new_easiness_factor_lowest_possible_easiness_factor_is_1_point_3(self):
self.scheduler.easiness_factor = 1.3
new_easiness_factor = self.scheduler._get_new_easiness_factor(0)
self.assertEqual(new_easiness_factor, 1.3)

def test_get_new_easiness_factor_calculates_correctly(self):
easiness_factor = self.scheduler._get_new_easiness_factor(0)
self.assertAlmostEqual(easiness_factor, 1.7)
easiness_factor = self.scheduler._get_new_easiness_factor(1)
self.assertAlmostEqual(easiness_factor, 1.96)
easiness_factor = self.scheduler._get_new_easiness_factor(2)
self.assertAlmostEqual(easiness_factor, 2.18)
easiness_factor = self.scheduler._get_new_easiness_factor(3)
self.assertAlmostEqual(easiness_factor, 2.36)
easiness_factor = self.scheduler._get_new_easiness_factor(4)
self.assertAlmostEqual(easiness_factor, 2.5)
easiness_factor = self.scheduler._get_new_easiness_factor(5)
self.assertAlmostEqual(easiness_factor, 2.6)

def test_get_new_days_until_next_review_card_is_to_be_seen_again_today_if_rating_is_below_3(self):
days_until_next_review = self.scheduler._get_new_days_until_next_review(2)
self.assertEqual(days_until_next_review, 0)

def test_get_new_days_until_next_review_card_is_to_be_seen_tomorrow_if_it_has_only_been_repeated_once(self):
self.scheduler.number_of_repetitions = 1
days_until_next_review = self.scheduler._get_new_days_until_next_review(3)
self.assertEqual(days_until_next_review, 1)

def test_get_new_days_until_next_review_card_is_to_be_seen_in_6_days_if_it_has_only_been_repeated_twice(self):
self.scheduler.number_of_repetitions = 2
days_until_next_review = self.scheduler._get_new_days_until_next_review(3)
self.assertEqual(days_until_next_review, 6)

def test_get_new_days_until_next_review_calculates_correctly(self):
self.scheduler.number_of_repetitions = 3
self.scheduler.days_until_next_review = 6
self.scheduler.easiness_factor = 2.5
days_until_next_review = self.scheduler._get_new_days_until_next_review(3)
self.assertEqual(days_until_next_review, 6*2.5)

if __name__ == '__main__':
unittest.main()

• In addition to help here - you can look into a coverage measurement tools, like coverage.py to spot branches you did not cover with tests. – Ruslan Osipov Mar 27 '14 at 3:58

Drop one-time use local variables and use libraries such as PyHamcrest to improve assertion readability.

Compare

def test_init_last_review_date_is_today(self):
assert_that(self.scheduler.last_review_date, is(datetime.date.today))


with the original

def test_init_last_review_date_is_today(self):
self.assertEqual(self.scheduler.last_review_date, datetime.date.today())


BTW, does assertEqual follow xUnit conventions and put the expected value before the actual value? If so, your assertion error messages will be backwards: "expected 'foo' but got 'bar'" when testing the stub method bar() { return "foo"; }

• For the sake of fairness I have to add that this is a matter of preference. – Ruslan Osipov Mar 27 '14 at 3:49
• @RuslanOsipov Aren't all code reviews a combination of personal and group preferences? Following best practices includes simplifying all code IMHO. TDD purists would say this comes naturally from creating fixtures on the fly and refactoring test code. When the tests provide the best examples of using your API, any work to improve them benefits every client. A cleaner implementation eases maintenance for the developers whereas a cleaner API improves the lives of everyone. – David Harkness Mar 27 '14 at 4:27
• I really don't get why people like the syntax pyHamcrest and similiar libraries provide. To me it feels akward relative the xUnit style. – Winston Ewert Mar 27 '14 at 5:25
• @WinstonEwert I like that Hamcrest assertions read like English prose: "assert that the computed value is what I expect" versus "assert equality for the value I expect and the one computed". Plus you can easily combine matchers: assert_that(myDict, has_entry("foo", greater_than(10))) – David Harkness Mar 27 '14 at 6:46
• @DavidHarkness, see I don't understand why people want their tests (or any code) to look like English. For your example, assert myDict["foo"] > 10 is way easier for me to parse then the mix of Python and English that your example provides. Its probably just me, but I don't get it. – Winston Ewert Mar 27 '14 at 15:48