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]

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):
        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):
        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):
        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__':
  • 1
    \$\begingroup\$ 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. \$\endgroup\$ Mar 27, 2014 at 3:58

1 Answer 1


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


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"; }

  • \$\begingroup\$ For the sake of fairness I have to add that this is a matter of preference. \$\endgroup\$ Mar 27, 2014 at 3:49
  • \$\begingroup\$ @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. \$\endgroup\$ Mar 27, 2014 at 4:27
  • 1
    \$\begingroup\$ I really don't get why people like the syntax pyHamcrest and similiar libraries provide. To me it feels akward relative the xUnit style. \$\endgroup\$ Mar 27, 2014 at 5:25
  • \$\begingroup\$ @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))) \$\endgroup\$ Mar 27, 2014 at 6:46
  • \$\begingroup\$ @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. \$\endgroup\$ Mar 27, 2014 at 15:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.