Generate random values and test them in Python 🐍

Gabor Szabo - Dec 14 '22 - - Dev Community

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)))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Running the tests

Now we can run the tests

PYTHONPATH=. pytest
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)))
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Indeed the result printed was different every time:

[6, 5, 1, 4]
[5, 4, 3, 2]
[5, 6, 2, 3]
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]

Enter fullscreen mode Exit fullscreen mode

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .