You are right, your tests should not verify that the
random module is doing its job; a unittest should only test the class itself, not how it interacts with other code (which should be tested separately).
It is of course entirely possible that your code uses
random.randint() wrong; or you be calling
random.randrange(1, self._sides) instead and your die never throws the highest value, but that'd be a different kind of bug, not one you could catch with a unittest. In that case, your
die unit is working as designed, but the design itself was flawed.
In this case, I'd use mocking to replace the
randint() function, and only verify that it has been called correctly. Python 3.3 and up comes with the
unittest.mock module to handle this type of testing, but you can install the external
mock package on older versions to get the exact same functionality
from unittest.mock import patch
# < python 3.3
from mock import patch
def _make_one(self, *args, **kw):
from die import Die
return Die(*args, **kw)
def test_standard_size(self, mocked_randint):
die = self._make_one()
result = die.roll()
def test_custom_size(self, mocked_randint):
die = self._make_one(sides=42)
result = die.roll()
if __name__ == '__main__':
With mocking, your test is now very simple; there are only 2 cases, really. The default case for a 6-sided die, and the custom sides case.
There are other ways to temporarily replace the
randint() function in the global namespace of
Die, but the
mock module makes this easiest. The
@mock.patch decorator here applies to all test methods in the test case; each test method is passed an extra argument, the mocked
random.randint() function, so we can test against the mock to see if it it indeed has been called correctly. The
return_value argument specifies what is returned from the mock when it is called, so we can verify that the
die.roll() method indeed returned the 'random' result to us.
I've used another Python unittesting best practice here: import the class under test as part of the test. The
_make_one method does the importing and instantiation work within a test, so that the test module will still load even if you made a syntax error or other mistake that'll prevent the original module to import.
This way, if you made a mistake in the module code itself, the tests will still be run; they'll just fail, telling you about the error in your code.
To be clear, the above tests are simplistic in the extreme. The goal here is not to test that
random.randint() has been called with the right arguments, for example. Instead, the goal is to test that the unit is producing the right results given certain inputs, where those inputs include the results of other units not under test. By mocking the
random.randint() method you get to take control over just another input to your code.
In real world tests, the actual code in your unit-under-test is going to be more complex; the relationship with inputs passed to the API and how other units are then invoked can be interesting still, and mocking will give you access to intermediate results, as well as let you set the return values for those calls.
For example, in code that authenticates users against a 3rd party OAuth2 service (a multi-stage interaction), you want to test that your code is passing the right data to that 3rd party service, and lets you mock out different error responses that that 3rd party service would return, letting you simulate different scenarios without having to build a full OAuth2 server yourself. Here it is important to test that information from a first response have been handled correctly and have been passed on to a second stage call, so you do want to see that the mocked service is being called correctly.