Search code examples
pythonpython-mock

Python Mock Iterable (sort of) not working


I am creating a test module for a Flask application. In this Resource's get() method, I fetch data from Mongo and then iterate through it to produce output. I am mocking the collection's find() method to return my iterable. The problem is that inside get() where I loop through the iterable, it skips as though it was empty. So I tried to loop through the iterable inside the test instead and I can successfully see the 3 dicts that it should contain.

Class properties:

class _TestAll(BaseAllReports):
    collection = MagicMock()
    bool_columns = ('bool1', 'bool2')
    string_set_columns = ('string1', 'string2')
    int_columns = ('int1', 'int2')
    text_columns = ('text1', 'text2')
    stats_columns = ('bool1', 'int1')

Resource.get():

def get(self):
    args = self.parser().parse_args()
    search = self.search_doc(args)

    docs = self.collection.find(search, {'_id': False})
    print(docs)

    ids, total_hurt, total_dead = set(), 0, 0
    stats = dict((x, {}) for x in self.stats_columns)
    stats['month'] = {}
    for d in docs:
        print('get', d)
        if d['id'] in ids:
            continue
        else:
            ids.add(d['id'])
        for s in self.stats_columns:
            if s in self.bool_columns:
                key = u'Sí' if d[s] else 'No'
            else:
                key = d[s]
            number = stats[s].get(key, 0) + 1
            stats[s][key] = number

        month_key = d['timestamp'].strftime('%b')
        month_summary = stats['month'].get(month_key, {'hurt': 0, 'dead': 0})
        month_summary['hurt'] += d['total_hurt']
        month_summary['dead'] += d['total_dead']
        stats['month'][month_key] = month_summary

        total_hurt += d['total_hurt']
        total_dead += d['total_dead']
    return {
        'incidents': len(ids),
        'involved': docs.count(),
        'affected': total_hurt + total_dead,
        'hurt': total_hurt,
        'dead': total_dead,
        'stats': stats
    }

Test setup:

@classmethod
def setUpClass(cls):
    app.testing = True
    cls.app = app.test_client()
    cls.url = '/incidents'
    cls.url_with_key = '/incidents?key=testKeyHash'
    api.add_resource(_TestAll, cls.url)

Test:

def test_get(self):
    with patch('__main__._TestAll.collection.find') as search:
        answer = []
        for i in range(3):
            answer.append({
                'id': i,
                'bool1': True, 'bool2': False,
                'string1': 'test', 'string2': 'test',
                'int1': 1, 'int2': 2,
                'text1': 'test', 'text2': 'test',
                'timestamp': datetime.now(), 'total_hurt': 1, 'total_dead': 0})
        search.__iter__.return_value = answer
        search.return_value.count.return_value = len(answer)
        response = self.app.get(self.url_with_key)
        data = json.loads(response.data.decode())
        for i in search:
            print('test', i)
        print(data)
        self.assertEqual(_TestAll.collection.find.call_count, 1)
        self.assertIn('stats', data)
        for s in _TestAll.stats_columns:
            self.assertIn(s, data['stats'])

Terminal output:

<MagicMock name='find()' id='4423760080'>
('test', {'timestamp': datetime.datetime(2017, 5, 25, 13, 3, 9, 255912), 'text2': 'test', 'text1': 'test', 'int1': 1, 'int2': 2, 'id': 0, 'bool1': True, 'bool2': False, 'total_hurt': 1, 'total_dead': 0, 'string2': 'test', 'string1': 'test'})
('test', {'timestamp': datetime.datetime(2017, 5, 25, 13, 3, 9, 255923), 'text2': 'test', 'text1': 'test', 'int1': 1, 'int2': 2, 'id': 1, 'bool1': True, 'bool2': False, 'total_hurt': 1, 'total_dead': 0, 'string2': 'test', 'string1': 'test'})
('test', {'timestamp': datetime.datetime(2017, 5, 25, 13, 3, 9, 255928), 'text2': 'test', 'text1': 'test', 'int1': 1, 'int2': 2, 'id': 2, 'bool1': True, 'bool2': False, 'total_hurt': 1, 'total_dead': 0, 'string2': 'test', 'string1': 'test'})
{u'stats': {u'bool1': {}, u'int1': {}, u'month': {}}, u'involved': 3, u'dead': 0, u'hurt': 0, u'incidents': 0, u'affected': 0}

I don't understand why the Resource doesn't loop properly through the iterable but the test can't. Any help is appreciated.

Thanks


Solution

  • When setting the __iter__ value, the line was

    search.__iter__.return_value = answer
    

    I didn't take into account the fact that filter() was callable. The right way to achieve what I was trying is:

    search.return_value.__iter__.return_value = answer
    

    Because the search Mock was being called, a new MagicMock was returned, which obviously didn't have the __iter__ property set. The reource's get() and the test function were accessing different mocks that's why it only worked on one of them.

    The way I found out was by printing the mock inside the test method as well and getting a different mock id for it.