The next thing I wanted to do is to generate the random values the computer hides that the user will have to guess. In the original description I describe that both the number of the hidden items and the type of their value is flexible, but at first let's use only 4 different digits between 1-6.
Test the random values
The first thing was to add the a test case to the already existing test file, that will verify that the, not yet existing new_secret
will return the correct results.
However, we have a problem. How can we test a function that will return a random set of values?
In our case we could check
- if the correct number of values were returned
- if they are distinct
- if they are from the same pool of data
def test_new_secret():
secret = mm.new_secret()
assert len(secret) == 4
assert len(set(secret)) == 4
assert set(secret).issubset(list(range(1, 7)))
If we run pytest
now it will obviously fail as the new_secret
has not been implemented yet, but we can do that.
The implementation
It is actually shorter than the test:
import random
def new_secret():
pool = list(range(1, 7))
return random.sample(pool, 4)
Running the tests
Now we can run the tests
PYTHONPATH=. pytest
Let's see the full files:
mastermind/game.py
import random
def play():
...
def new_secret():
pool = list(range(1, 7))
return random.sample(pool, 4)
tests/test_game.py
import mastermind.game as mm
def test_game():
mm.play()
assert True
def test_new_secret():
secret = mm.new_secret()
assert len(secret) == 4
assert len(set(secret)) == 4
assert set(secret).issubset(list(range(1, 7)))
The problem of duplication
I feel a bit that this test is a bit of overdoing things, but it will be useful to have when we'll start to allow the user to have different size of hidden value and different types of hidden values.
The more problematic thing is that several values were duplicated. For example the pool and the size of hidden value.
Create global constants
One solution to this is to move these values to global variables or global constants. Let's see that:
mastermind/game.py
import random
POOL = list(range(1, 7))
SIZE = 4
def play():
...
def new_secret():
return random.sample(POOL, SIZE)
tests/test_game.py
import mastermind.game as mm
def test_game():
mm.play()
assert True
def test_new_secret():
secret = mm.new_secret()
assert len(secret) == mm.SIZE
assert len(set(secret)) == mm.SIZE
assert set(secret).issubset(mm.POOL)
I used upper-case variable names as in python the convention is that constants (basically variable that should not change) are named in upper case.
In a way this is better solution as we don't repeat the number 4 and the creation of the POOL, but now that they are global we won't be able to set them without using some nasty code.
We'll have to deal with that later.
In addition now the test depends on the code generating the POOL
correctly.
Test the exact returned values
Let's add another test case which is a lot more fixed. In which we check the exact values returned by the new_secret
function.
The problem is that these are random values so they will be different every time we run the test. To see this I added a print
statement to the test code:
secret = mm.new_secret()
print(secret)
Then I ran pytests
several times with the -s
flag. That tells pytest
to let through the output that goes to STDOUT and STDERR.
PYTHONPATH=. pytest -s
Indeed the result printed was different every time:
[6, 5, 1, 4]
[5, 4, 3, 2]
[5, 6, 2, 3]
Test by fixing the random generator
You might already know that the random
module of Python generates Pseudo-random numbers and that the random.sample()
function uses these Pseudo-random numbers to select the sample.
You might also know that using random.seed()
we can fix where the Pseudo-random number generator starts.
So we can write a test where do just that:
import random
def test_fixed_secret():
random.seed(42)
secret = mm.new_secret()
print(secret)
The 42 was selected in a totally arbitrary way.
This time, no matter how many times we run the test we will get back the exact same result:
[6, 1, 5, 3]
So our new test will look like this:
def test_fixed_secret():
random.seed(42)
secret = mm.new_secret()
assert secret == [6, 1, 5, 3]
Now we have two separate test functions. One reuses the current values of the POOL and SIZE, the other one expect a very specific result.
The whole test file
Just for completeness, here is the whole test file as we have it now:
import random
import mastermind.game as mm
def test_game():
mm.play()
assert True
def test_new_secret():
secret = mm.new_secret()
assert len(secret) == mm.SIZE
assert len(set(secret)) == mm.SIZE
assert set(secret).issubset(mm.POOL)
def test_fixed_secret():
random.seed(42)
secret = mm.new_secret()
# print(secret)
assert secret == [6, 1, 5, 3]
Conclusion
There are choices in the implementation that will impact how nice our code will look like, but having tests will make it much safer to change our code as we will be able to easily verify that we did not break anything.
In the meantime you can try to play with the above code. You could enable the print
statement, change the value of the seed
, run the tests and see how for every seed we get a different response.
See you next part