How to test class methods "independently" in Python

Question:

I am struggling to get my head around testing class methods "independently".

Say, I have this class:

class Counter:
    def __init__(self, start = 0):
        self.number = start

    def increase(self):
        self.number += 1

How do I sensibly test the method increase()?

Some time ago, a senior developer told me (and maybe I misunderstood) that I should be testing my methods independently, so that, say, if some parts of the class change my method should still test OK.

This led me to test methods in a slightly cumbersome way:

# Using pytest here

def test_increase():
    class MockCounter:
        def __init__(self):
            self.number = 0

    x = MockCounter()
    Counter.increase(x)
    assert x.number == 1

where, basically:

  • I mock the class Counter with MockCounter (so the class Counter is not being a dependency which might break my test);
  • I call the method to test as it was a static method.

It works, but I have the feeling I have misunderstood quite a lot here.

What am I getting wrong?

Asked By: user3648203

||

Answers:

You need to think about what kinds of changes would cause your test to fail. For example, taking the current implementation:

class Counter:
    def __init__(self, start = 0):
        self.number = start

    def increase(self):
        self.number += 1

you could test the class simply like:

def test_increase():
    counter = Counter()
    counter.increase()
    assert counter.number == 1

What kinds of changes would cause that test to fail with a false negative? How about a change to the default value:

class Counter:
    def __init__(self, start = 1):
                             # ^ pre-increased for your convenience 
        self.number = start

    def increase(self):
        self.number += 1

assert counter.number == 1 now fails because it’s actually 2, but the increase method hasn’t changed and still works correctly. Assuming you had another test like:

def test_default_value():
    counter = Counter()
    assert counter.number == 0

you’d now have two failing tests, one saying the default value is wrong and the other saying the increase method doesn’t work. In this simple example it’s clear that the latter is caused by the former and is a false negative, but in larger and more complex classes it can be really helpful to have better triangulation of where a given problem actually is, which is what leads to the advice to test methods independently.

However, you don’t want to mock parts of the thing you’re supposed to be testing, as you currently do with the MockCounter, because the point of the exercise is to increase your confidence in the actual implementation rather than the test double (keep test doubles for collaborating objects, rather than parts of the object under test). In the worst case you end up with tests that only exercise test doubles, giving a false sense of confidence in an implementation that may or may not actually work.

So let’s reframe the desired behaviour in terms of the invariants. After calling increase, the number attribute shouldn’t always be 1, it should be one higher than before. Expressing that in a test that’s resilient to the above change:

  • Store the starting value before increasing and compare to that:

    def test_increase():
        counter = Counter()
        before = counter.number
        counter.increase()
        assert counter.number == before + 1  # now 2, but that's irrelevant here
    
  • Alternatively you could consider the default value in __init__ irrelevant to the behaviour of the increase method, and use an explicit starting point in your test instead:

    def test_increase():
        counter = Counter(0)
        counter.increase()
        assert counter.number == 1
    

Either way you’d have a more robust test without having to introduce an awkward partial mock.

Answered By: jonrsharpe
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.