1

There's a situation where I want to check how many times an internal class method has been called. I have a sensitive cloud task that must be done in a count that depends upon some circunstances. I would like to strengthen my application with an unittest to assert the number of times a specific function has been called.

To do so in a much simpler scenario, I would like to make a test in the following script:

class HMT:

    def __init__(self):
        self.buildedList = []

    def handle(self, string_to_explode: str):
        for exploded_part in string_to_explode.split(","):
            self.doCoolThings(exploded_part)

    def doCoolThings(self, fetched_raw_string: str):
        self.buildedList.append("Cool thing done: " + fetched_raw_string)

Depending on the string that I deliver to the handle function, the doCoolThings will be called N times (in this simple case, depends solely in the number of comas inside the string).

I can make a test works by just counting the number of resulting elements inside builderList:

import unittest
from HMT import HMT

class test_HMT(unittest.TestCase):

    def setUp(self):
        self.hmt = HMT()

    def test_hmt_3(self):
        string_to_test = "alpha,beta,gamma"
        self.hmt.handle(string_to_test)
        self.assertEqual(3, len(self.hmt.buildedList))

    def test_hmt_2(self):
        string_to_test = "delta,epsilon"
        self.hmt.handle(string_to_test)
        self.assertEqual(2, len(self.hmt.buildedList))

But in the real scenario, there's will not have an available public class list that always will match its number of elements to the times the function doCoolThings was called.

So, how do I check how many times the doCoolThings was called without needing to check the list elements count?

I know that I can just put a counter in the class that is increased each time doCoolThings is called and expose it externally to be checked afterwards. But I don't would like to mess up the code putting lines that is not directly related to my business rule.

After @jarmod comment, I came into this code's version:

def mydecorator(func):
    def wrapped(*args, **kwargs):
        wrapped.calls += 1
        return func(*args, **kwargs)
    wrapped.calls = 0
    return wrapped

class HMT:

    def __init__(self):
        self.buildedList = []

    def handle(self, string_to_explode: str):
        for exploded_part in string_to_explode.split(","):
            self.doCoolThings(exploded_part)

    @mydecorator
    def doCoolThings(self, fetched_raw_string: str, *args, **kwargs):
        self.buildedList.append("Cool thing done: " + fetched_raw_string)

And the test:

import unittest
from HMT import HMT

class test_HMT(unittest.TestCase):

    def test_hmt_3_dec(self):
        hmt = HMT()
        string_to_test = "epsilon,ota,eta"
        hmt.handle(string_to_test)
        self.assertEqual(3, hmt.doCoolThings.calls)

    def test_hmt_3(self):
        hmt = HMT()
        string_to_test = "alpha,beta,gamma"
        hmt.handle(string_to_test)
        self.assertEqual(3, len(hmt.buildedList))

But still are not working properly. When I run tests, I receive:

.F
======================================================================
FAIL: test_hmt_3_dec (myTest.test_HMT)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "D:\Users\danil\tmp\myDec\myTest.py", line 10, in test_hmt_3_dec
    self.assertEqual(3, hmt.doCoolThings.calls)
AssertionError: 3 != 6

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

More tests shows that the tests runs twice but the counter do not reset either in new instantiation.

Anyway, the initial idea was to dynamically put an observer in a internal class method and externaly fetches each time when those method is triggered (still, not something that seems to be solved using decorator).

Thanks alot for answers (and as a plus to leave powered up, if someone knows how to reset the counter in the decorator I will appreciate as well).

danilocgsilva
  • 187
  • 1
  • 13
  • Perhaps you could use a 'counted' decorator to limit the modifications to the base code. Example: https://stackoverflow.com/questions/21716940/is-there-a-way-to-track-the-number-of-times-a-function-is-called – jarmod Oct 04 '20 at 23:51

1 Answers1

1
from HMT import HMT


count = 0
def counter(f):
    def wrap(*args, **kwargs):
        global count
        count += 1
        return f(*args, **kwargs)
    return wrap


class test_HMT(unittest.TestCase):

    def setUp(self):
        self.hmt = HMT()
        
        # Add decorator.
        self.hmt_no_decorator = self.hmt.doCoolThings
        self.hmt.doCoolThings = counter(self.hmt.doCoolThings)
        
    def test_doCoolThings_count(self):
        repeat = 3               
        [self.hmt.doCoolThings() for _ in range(repeat)]

        self.assertEqual(counter, repeat)

    def tearDown(self):
        # Remove decorator.
        self.hmt.doCoolThings = self.hmt_no_decorator

        ...
  • doCoolThings is not modified in the business code. You simply get the counting behaviour for testing.
  • You can get rid of the global var by replacing the count var with an object.. or by doing anything else really. But does it matter in testing?
Florian Fasmeyer
  • 795
  • 5
  • 18