0

I am looking to test some code in Python - specifically class methods.

I have a class that looks like

class BigBoyClass:
   def __init__(self, ...{configuration objects}...):
       ...{expensive init}...
       self.init_attr1 = ...
       self.init_attr2 = ...

   def method1(self):
      self.new_attr1 = ...
      self.new_attr2 = ...

To test method1 I want to pass it a "self" with particular values in init_attr1 and init_attr2

So far I've been doing:

class DummyContainer:
    def __init__(self):
       pass

c = DummyContainer()
c.init_attr1 = {v1}
c.init_attr2 = {v2}

BigBoyClass.method1(c)

assert c.new_attr1 == {v3}
assert c.new_attr2 == {v4}

But I get a feeling I'm re-inventing the wheel here. Is there a better way to do this?

MYK
  • 1,988
  • 7
  • 30

1 Answers1

0

Indeed. If you can refactor the BigBoyClass.__init__ method, you can do better than mocks !

Based on your example, here is my initial situation :

class BigBoyClass:
   def __init__(self, big_number: int):
       sum(range(big_number))  # something slow
       self.init_attr1 = big_number  # whatever
       self.init_attr2 = big_number + 1  # whatever

   def method1(self):
      self.new_attr1 = 4  # whatever
      self.new_attr2 = 5  # whatever


def test_method1():
    # given
    instance = BigBoyClass(10**9)  # takes a few seconds on my computer

    # when
    instance.method1()

    # then
    assert (instance.new_attr1 == 4) and (instance.new_attr2 == 5)

What I want is a dumb __init__ : it is given parameters, it just sets them to the corresponding self fields. The complex part is moved to a classmethod that will handle the complex part.

class BigBoyClass:
    def __init__(self, attr1: int, attr2: int):
        # removed the slow part here
        self.init_attr1 = attr1
        self.init_attr2 = attr2

    def method1(self):
        self.new_attr1 = 4  # whatever
        self.new_attr2 = 5  # whatever

    @classmethod
    def init_with_config_object(cls, big_number: int):
        sum(range(big_number))  # something slow
        return cls(big_number, big_number+1)


def test_method1():
    # given
    instance = BigBoyClass(10**9, 10**9+1)  # lightning-fast !

    # when
    instance.method1()

    # then
    assert (instance.new_attr1 == 4) and (instance.new_attr2 == 5)

I did not change my test at all, but because I changed the __init__ signature, now I have to adapt all the other existing instantiations of the BigBoy class, to call the init_with_config_object instead of just the class itself.

What you gain from doing that :

  • no need to mock, so it's easier to write tests
  • no mocks, so that you don't have to maintain them when your actual class change
  • you can check separately that the configuration initializes correctly, and that the method does what expected
  • you can add other classmethods if you need to be able to instantiate the same class but in different ways
  • if providing the values for the init_attr of your object is complex, you can create test factories : functions that will handle that for you, using a direct call to __init__ anyway

I don't know if this technique has a name, but it looks a bit like a "java beans" because it provides the way to be constructed in any way, without restrictions on signatures/protocols.

Lenormju
  • 4,078
  • 2
  • 8
  • 22