It is good practice to reuse unit test for different functions in Python

I'm new to unit testing and want to start with a nose frame . But answers using unittest , pytest are welcome too. Also, of course, general advice.

I think I am getting the basic concept, but I am lacking practice in setting up a good test. I am also struggling with how to deploy tests. I'm especially unsure about how to approach the case where I want to run multiple test cases on a different module function:

For example: I might have a module called diceroller.py

it contains a couple of functions to simulate rolling, change and check results, etc. All dice rolling functions must run through the same tests (whether they return integers with the right number of values, values ​​in a range, etc.). But some of them should also be directed against some additional cases.

So I got a subdir test

and want to install my test code there. How do I approach this?

# a section of `diceroller.py`

def roll_a_dice(sides=6):
  """Return an integer x where `x >= 1 <= sides`"""
  return random.randint(1, sides)

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  rolls = list()
  while count:
    rolls.append(random.randint(1, sides))
    count -= 1
  return rolls

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  some_rolls = list()
  while times:
    some_rolls.append(roll_dice(sides, count))
    times -= 1
  return some_rolls

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield random.randint(1, sides)

      

Small update

Simeon Visser has a good point. But the above code, where it just functions, also contains some context for my Questions, namely: how can I (re) use test cases for different functions?

I am guessing that writing tests like check_xyz

and then calling it from test_a

and test_b

for example is the simplest solution? Or is it bad practice?

Rik Poggi's solution seems to do what it was trying to do (will play with it immediately after entering this text). But I have a feeling it "complicates" things for a lot of people ... maybe not technically, but it might seem "too big".

+3


source to share


2 answers


I will not consider the problem that the test code is clearly having to check. I will focus on your question about test code reuse.

Important things to keep in mind before starting:

  • Tests should be simple (no or very little logic inside).
  • Tests must be readable (they are like code documentation).
  • Tests must be maintainable.

The simplicity of your tests is a must, and in doing so, you need to find the right balance between the last two points.

One final warning: Be very careful when reusing test code, because if you do it wrong, your tests will listen. Test errors are hard to spot, but after a year you may find one, a seed of doubt will be planted and the credibility of your tests may diminish. Tests that you do not trust ("oh, that fails, but unreasonable because the other fails too, etc.") Completely and completely useless.


Now that we've cleared our path, let's take a look at your code. Usually the dynamic part of the tests is achieved through the context (methods setUp

and tearDown

), but in your case it's a little more complicated.

I want to run multiple test cases on different module functions.

You don't really want the same test case to run with a different function, but only with the same code. A good test environment will have (at least) one test case for each feature.

Since you are looking for the ability to run a previous test case / package against the partial output of another function, you will need functools.partial

one that will allow you to wrap your function with default arguments.

This means that you should start at the bottom, starting with the simplest tests:

def check_valid_range(value, sides):
    """Check that value is a valid dice rolling"""
    assert 0 < value <= sides

def check_is_int(value):
    """Check that value is an integer"""
    assert type(value) is int

      



And then build on top of them (with little connectivity):

class TestRollADice:
   """roll_a_dice basic tests"""

    @staticmethod
    def func_to_test(*args, **kwargs):
        return diceroller.roll_a_dice(*args, **kwargs)

    @staticmethod    
    def check_valid_output(value, sides):
        """Check that value is the self.function is valid"""
        yield check_valid_range, value, sides
        yield check_is_int, value

    def test_3sides(self):
        """Check valid result for a 3 sides dice"""
        yield self.check_valid_output, self.func_to_test(3), 3

    def test_list_valid_sides(self):
        """Check valid result for a list of valid sides (sides >= 3)"""
        sides = list(range(3, 13))
        for s in sides:
            yield self.check_valid_output, self.func_to_test(s), s

    def test_0_sides_raises_ValueError(self):
        """0 side dice raise ValueError"""
        assert_raises(ValueError, self.func_to_test, 0)

    def test_1_sides_raises_ValueError(self):
        """1 side dice raise ValueError"""
        assert_raises(ValueError, self.func_to_test, 1)

    def test_2_sides_raises_ValueError(self):
        """2 sides dice raise ValueError"""
        assert_raises(ValueError, self.func_to_test, 2)

    def test_minus1_sides_raises_ValueError(self):
        """-1 side dice raise ValueError"""
        assert_raises(ValueError, self.func_to_test, -1)

      

This is the minimum number of tests your function should have roll_a_dice

. Try running nosetests

in your package and you will see two of them don't work, see how important the tests are! :)

I'm sure you noticed how the test name is verbose for readability.

Now for the roll_dice

main test class has to test these three, four main values ​​and baisc errors, because the good thing should be simple and keep under control what is being tested, I would say that you might need some functionality like:

def check_list_length(self, lst, count):
    """Check that the len(lst) == count"""
    assert len(lst) == count

def check_valid_count(self, count):
    """Check count > 0"""
    assert count > 0

class TestRollDice:
    # ...

      

And now, if you want to reuse old code, you can subclass TestRollADice

:

from functools import partial

class TestRollDice_Count1(TestRollADice):

    @staticmethod
    def func_to_test(*args, **kwargs):
        return partial(diceroller.roll_dice, count=1)(*args, **kwargs) 

      

And voilΓ , almost free, you will have twice as many tests :)

Note:

  • All of the above codes can be written in the language unittest

    , but since you asked about nose

    , I wrote with that.
  • There is a small problem: the docstras for passing tests are the same, so they appear twice in verbose output. This is not a big problem, as if one of these tests fails, you will be prompted for a test address and that it is unique (and clearly, because luckily our test names are verbose). I also remember that docstrings can be disabled or something. So again, this is not a big problem and it should be easy for you to find a workaround if needed.
+6


source


Your code can be refactored to reduce the amount of code, and therefore the amount of code that needs to be tested. Let's see what you want:

  • Roll the dice ( roll_a_dice

    )
  • Roll the dice several times ( roll_dice

    )
  • Get some rolls where the cubes have been rolled multiple times ( roll_some_dice

    )

Let's rewrite it first roll_dice

. We basically want to call roll_a_dice

multiple times and put the results in a list:

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  return [roll_a_dice(sides) for i in xrange(count)]

      

Do the same with roll_some_dice

: we want to call roll_dice

multiple times and put the results in a list:

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  return [roll_dice(sides, count) for i in xrange(times)]

      



Finally, rolling_dice

it still contains some logic:

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield roll_a_dice(sides)

      

The code is now much easier to check for errors and easier to unit test:

import random

def roll_a_dice(sides=6):
  """Return an integer x where `x >= 1 <= sides`"""
  return random.randint(1, sides)

def roll_dice(sides=6, count=1):
  """Return a list of integers (most function except this)"""
  return [roll_a_dice(sides) for i in xrange(count)]

def roll_some_dice(sides=6, count=1, times=1):
  """Return a list of list containing integers"""
  return [roll_dice(sides, count) for i in xrange(times)]

def rolling_dice(sides=6, count=1):
  """Yielding integers `count` times"""
  while count:
    count -= 1
    yield roll_a_dice(sides)

      

Now you can write a test case to check if the dice rolls correctly ( roll_a_dice

). Once you've tested this, you no longer need to do this for other functions. For these functions, you only need to check if the correct number of results was received (because any wrong values ​​should have been caught by the test case (s) for roll_a_dice

).

Likewise, when you test roll_some_dice

, you can assume what is roll_dice

working correctly because you have already written tests for that function. If something goes wrong, you can add the test to the test case for roll_dice

, not roll_some_dice

(unless the problem is defined for roll_some_dice

).

+4


source







All Articles