Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mocking a class's construction

I'm just getting started with using Python's mock library to help write more concise and isolated unit tests. My situation is that I've got a class that reads in data from a pretty hairy format, and I want to test a method on this class which presents the data in a clean format.

class holds_data(object):
    def __init__(self, path):
        """Pulls complicated data from a file, given by 'path'.

        Stores it in a dictionary. 
        """
        self.data = {}
        with open(path) as f:
            self.data.update(_parse(f))

    def _parse(self, file):
        # Some hairy parsing code here
        pass

    def x_coords(self):
        """The x coordinates from one part of the data
        """
        return [point[0] for point in self.data['points']]

The code above is a simplification of what I have. In reality _parse is a fairly significant method which I have test coverage for at the functional level.

I'd like to be able, however, to test x_coords at a unit test level. If I were to instantiate this class by giving it a path, it would violate the rules of unit tests because:

A test is not a unit test if:

  • It touches the filesystem

So, I'd like to be able to patch the __init__ method for holds_data and then just fill in the part of self.data needed by x_coords. Something like:

from mock import patch
with patch('__main__.holds_data.__init__') as init_mock:
    init_mock.return_value = None
    instance = holds_data()
    instance.data = {'points':[(1,1),(2,2),(3,4)]}
    assert(instance.x_coords == [1,2,3])

The code above works but it feels like it's going about this test in a rather roundabout way. Is there a more idiomatic way to patch out a constructor or is this the correct way to go about doing it? Also, is there some code smell, either in my class or test that I'm missing?

Edit: To be clear, my problem is that during initialization, my class does significant amounts of data processing to organize the data that will be presented by a method like x_coords. I want to know what is the easiest way to patch all of those steps out, without having to provide a complete example of the input. I want to only test the behavior of x_coords in a situation where I control the data it uses.

My question of whether or not there is code smell here boils down to this issue:

I'm sure this would be easier if I refactor to have x_coords be a stand alone function that takes holds_data as a parameter. If "easier to tests == better design" holds, this would be the way to go. However, it would require the x_coords function to know more about the internals of holds_data that I would normally be comfortable with. Where should I make the trade off? Cleaner code or cleaner tests?

like image 822
Wilduck Avatar asked Apr 02 '12 17:04

Wilduck


People also ask

Can you mock a constructor?

0, we can now mock Java constructors with Mockito. This allows us to return a mock from every object construction for testing purposes. Similar to mocking static method calls with Mockito, we can define the scope of when to return a mock from a Java constructor for a particular Java class.

What is mocking a class?

Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.

How do you mock a class constructor Jest?

In order to mock a constructor function, the module factory must return a constructor function. In other words, the module factory must be a function that returns a function - a higher-order function (HOF). Since calls to jest. mock() are hoisted to the top of the file, Jest prevents access to out-of-scope variables.

How do I create a class mock?

We can use Mockito class mock() method to create a mock object of a given class or interface. This is the simplest way to mock an object. We are using JUnit 5 to write test cases in conjunction with Mockito to mock objects.


2 Answers

Since you're only interested in testing one method, why don't you just mock the entire HoldsData class and pin on it the x_coords method?

>>> mock = MagicMock(data={'points': [(0,1), (2,3), (4,5)]})
>>> mock.x_coords = HoldsData.__dict__['x_coords']
>>> mock.x_coords(mock)
[0, 2, 4]

This way you'll have full control over the x_coords input and output (either by side effect or return value).

Note: In py3k you could just do mock.x_coords = HoldsData.x_coords since there are no more unbound methods.

That could also be done in the constructor of the mock object:

MagicMock(data={'points': [(0,1), (2,3), (4,5)]}, x_coords=HoldsData.__dict__['x_coords'])
like image 92
Rik Poggi Avatar answered Sep 20 '22 20:09

Rik Poggi


You're basically running into this issue due to this:

A test is not a unit test if:

  • It touches the filesystem

If you wish to follow this rule, you should modify the _parse method. In particular, it should not take a file as input. The task of _parse is to parse the data but where the data comes from is not the concern of that method.

You could have a string that contains the same data which is then passed to _parse. Similarly, the data could come from a database or somewhere else entirely. When _parse only takes the data as input, you can unit test that method in a much easier way.

It would look something like this:

class HoldsData(object):
    def __init__(self, path):
        self.data = {}
        file_data = self._read_data_from_file(path)
        self.data.update(self._parse(file_data))

    def _read_data_from_file(self, path):
        # read data from file
        return data

    def _parse(self, data):
        # do parsing

Clean code leads to clean tests of course. The best case would be to mock the data and to provide _parse with input and then test x_coords later. If that's not possible, I'd leave it as it is. If your six lines of mock code are the only part of the test cases that you're worried about, then you're fine.

like image 34
Simeon Visser Avatar answered Sep 18 '22 20:09

Simeon Visser