boB Stepp <robertvst...@gmail.com> writes: > If I had a function to roll a die, such as: > > import random > > def roll_die(num_sides): > return random.randint(1, num_sides) > > How would I write unit tests for this?
You need to make the system deterministic during the test run. Since the random number generator (RNG) is a dependency that your test cases don't entirely control, that is a dependency that makes the system non-deterministic. One way to do that is to allow the dependency – the RNG – to be specified as part of the API of the system under test:: def roll_die(num_sides, rng=None): This is known as the “dependency injection” pattern. The system makes a deliberate API for its dependencies, and allows the caller to specify the object that represents the dependency. <URL:https://en.wikipedia.org/wiki/Dependency_injection> The system does not directly use the external dependency; it uses the dependency supplied, as an abstraction:: import random def roll_die(num_sides, rng=None): """ Return a result from a random die of `num_sides` sides. :param num_sides: The number of sides on the die. :param rng: An instance of `random.Random`, the random number generator to use. Default: the `random.random` instance. :return: The integer result from the die roll. The die is defined to have equal-probability faces, numbered from 1 to `num_sides`. """ if rng is None: rng = random.random result = rng.randint(1, num_sides) return result In this case the dependency has a sensible default, so the caller doesn't *need* to specify the dependency. That's not always true, and dependency injection often entails re-designing the API and callers must adapt to that new API. With that re-design, the test module can create an RNG that behaves as expected. Your test cases can create their own RNG instance and specify its seed, which means the same numbers will be generated every time from that seed. import unittest import random from . import die_roller # The system under test. class roll_die_TestCase(unittest.TestCase): """ Test cases for the `roll_die` function. """ def setUp(self): """ Set up test fixtures. """ # Set the seed value such that the system can't anticipate # it, but such that we can re-use it any time. self.seed = id(self) self.rng = random.Random(self.seed) def test_result_in_expected_range(self): """ The result should be in the range expected for the die. """ test_num_sides = 6 expected_range = range(1, test_num_sides + 1) result = die_roller.roll_die(test_num_sides, rng=self.rng) self.assertIn(result, expected_range) def test_result_is_from_rng(self): """ The result should be produced by the supplied RNG. """ test_num_sides = 6 self.rng.seed(self.seed) expected_result = self.rng.randint(1, test_num_sides) self.rng.seed(self.seed) result = die_roller.roll_die(test_num_sides, rng=self.rng) self.assertEqual(result, expected_result) > And I do not see how I can test for an appropriate "randomness" to the > numbers the function generates without to a lot of iterations, That's the thing about randomness. By definition, you can't determine it :-) But by providing your own RNG that is under control of the test cases, you can know what numbers it will produce by re-setting it to a known seed. Hopefully you can take this as an example of how to better design systems so they don't have tightly-entangled external dependencies. -- \ “The good thing about science is that it's true whether or not | `\ you believe in it.” —Neil deGrasse Tyson, 2011-02-04 | _o__) | Ben Finney _______________________________________________ Tutor maillist - Tutor@python.org To unsubscribe or change subscription options: https://mail.python.org/mailman/listinfo/tutor