constructor classmethods

teppo.pera at gmail.com teppo.pera at gmail.com
Tue Nov 8 18:01:32 EST 2016


> How is having 15 arguments in a .create() method better than having 15 arguments in __init__() ?
> So, if you use the create() method, and it sets up internal data structures, how do you test them?  In other words, if create() makes that queue then how do you test with a half-empty queue?
> Not all design patterns make sense in every language.
 
Seems that there is still some unclarity about the whole proposal, so I combine your questions into an example. But first, little more background.

Generally, with testing, it would be optimal to test outputs of the system for given inputs without caring how things are implemented. That way, any changes in implementation won't affect test results. Very trivial example would be something like this:

def do_something_important(input1, input2, input3)
    return  # something done with input1, input2, input3

Implementation of do_something_important can be one liner, or it can contain multiple classes, yet the result of the function is what matters. That's also the basic strategy (one of many) I try to follow with when testing the code I write.

Now, testing the class using queue (as an example) should follow same pattern, thus a simple example how to test it would look like this (assuming that everyone knows how to use mock library):

# This is just an example testing with DI and mocks.

class Example:
   def __init__(self, queue):
       self._queue = queue

   def can_add(self):
       return not self._queue.full()  

def TestExample(unittest.TestCase):
    def setUp(self):
        self.queue = mock.MagicMock()

    def test_it_should_be_possible_to_know_when_there_is_still_room_for_items(self):
       self.queue.full.return_value = False
       example = Example(self.queue)
       self.assertTrue(example.can_add())

    def test_it_should_be_possible_to_know_when_no_more_items_can_be_added(self):
       self.queue.full.return_value = True
       example = Example(self.queue)
       self.assertFalse(example.can_add())

In above example, example doesn't really care what class the object is. Only full method is needed to be implemented. Injected class can be Queue, VeryFastQueue or LazyQueue, as long as they implement method "full" (duck-typing!). Test takes advantage of that and changing the implementation won't break the tests (tests are not caring how Example is storing the Queue). 
 Also, adding more cases is trivial and should also make think the actual implementation and what is needed to be taken care of. For example, self.queue.full.return_value = None, or self.queue.full.side_effect = ValueError(). How should code react on those?

Then comes the next step, doing the actual DI. One solution is:

class Example:
    def __init__(self, queue=None):
        self._queue = queue or Queue()

Fine approach, but technically __init__ has two execution branches and someone staring blindly coverages might require covering those too. Then we can use class method too.

class Example:
    def __init__(self, queue):
        self._queue = queue

    @classmethod
    def create(cls):
        q = Queue()
        # populate_with_defaults
        # Maybe get something from db too for queue...
        return cls(q)

As said, create-method is for convenience. it can (and should) contain minimum set of arguments needed from user (no need to be 15 even if __init__ would require it) to create the object. It creates the fully functioning Example object with default dependencies. Do notice that tests I wrote earlier would still work. Create can contain slow executing code, if needed, but it won't slow down testing the Example class itself. 

Finally, if you want to be tricky and write own decorator for object construction, Python would allow you to do that.

@spec('queue')  # generates __init__ that populates instance with queue given as arg
class Example:
    @classmethod
    def create(cls):
        return cls(Queue())

Example can still be initialized calling Example(some_dependency), or calling Example.create() which provides default configuration. Writing the decorator would give unlimited ways to extend the class. And test written in the beginning of the post would still pass.



More information about the Python-list mailing list