0
votes

I'm writing a basic Django app that integrates with a third party API. Here is the basic folder structure of the files involved:

timecardsite/
    __init__.py
    tests/
        __init__.py
        tests.py
    services.py
    views.py
    ...

The services.py file contains various functions that interact with the third party API directly. They typically return dictionaries with the data I need to use in the views.

In my tests file, specifically in the class where I test the views, I'm trying to mock the return value of a few of these service functions, so ideally they won't even be called (I test the service functions directly in another class):

tests.py

from unittest.mock import patch
from django.test import TestCase

import timecardsite.services as services
from timecardsite.models import Account

...

class ViewsTests(TestCase):

    @patch('timecardsite.views.services.get_access_token')
    @patch('timecardsite.views.services.get_account_info')
    def test_auth_view_saves_tokens_and_account_info_to_db(self, mock_access, mock_account):

        # Mock both get_access_token and get_account_info
        code = generate_random_token()
        access_token = generate_random_token()
        refresh_token = generate_random_token()
        account_id = generate_random_token(length=5)
        name = 'Example name'

        mock_access.return_value = {
            'access_token': access_token,
            'refresh_token': refresh_token
        }

        mock_account.return_value = {
            'account_id': account_id,
            'name': name
        }

        self.client.get(f'/auth/?code={code}')

        self.assertEqual(Account.objects.count(), 1)
        new_account = Account.objects.first()
        self.assertEqual(new_account.account_id, account_id)
        self.assertEqual(new_account.name, name)
        self.assertEqual(new_account.access_token, access_token)
        self.assertEqual(new_account.refresh_token, refresh_token)

The view where these functions are called:

views.py

from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse

import timecardsite.services as services
from timecardsite.models import Account

def auth(request):
    # capture code from url
    code = request.GET.get('code')

    if code:
        # request tokens
        tokens = services.get_access_token(code)

        # request account info
        account_info = services.get_account_info(tokens['access_token'])

        # create Account object
        account = Account(
            account_id = account_info['account_id'],
            access_token = tokens['access_token'],
            refresh_token = tokens['refresh_token'],
            name = account_info['name']
        )
        account.save()

        return HttpResponseRedirect(reverse('dashboard'))

This is the error I get - clearly the mocked service functions are being called, and are failing because it's trying to hit the actual API with a fake code I generated.

..........E
======================================================================
ERROR: test_auth_view_saves_tokens_and_account_info_to_db (timecardsite.tests.tests.ViewsTests)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/holden/.pyenv/versions/3.7.10/lib/python3.7/unittest/mock.py", line 1256, in patched
    return func(*args, **keywargs)
  File "/Users/holden/Projects/timesheet/timecardsite/tests/tests.py", line 193, in test_auth_view_saves_tokens_and_account_info_to_db
    self.client.get(f'/auth/?code={code}')
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/test/client.py", line 739, in get
    response = super().get(path, data=data, secure=secure, **extra)
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/test/client.py", line 395, in get
    **extra,
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/test/client.py", line 470, in generic
    return self.request(**r)
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/test/client.py", line 716, in request
    self.check_exception(response)
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/test/client.py", line 577, in check_exception
    raise exc_value
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/Users/holden/.pyenv/versions/timesheet/lib/python3.7/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/Users/holden/Projects/timesheet/timecardsite/views.py", line 19, in auth
    account_info = services.get_account_info(tokens['access_token'])
KeyError: 'access_token'

----------------------------------------------------------------------
Ran 11 tests in 0.074s

How do I successfully mock those functions?

Thanks.

1

1 Answers

0
votes

Figured it out. Once I patched the entire services module it successfully hooked in; however, I started getting another error because the Account.save() in the view was trying to save a MagicMock function to the database. All in all the test now reads like this:

    def test_auth_view_saves_tokens_and_account_info_to_db(self):
        # Mock both get_access_token and get_account_info
        tokens = {
            'access_token': generate_random_token(),
            'refresh_token': generate_random_token()
        }

        account_info = {
            'account_id': generate_random_token(length=5),
            'name': 'Example Name'
        }
        code = generate_random_token()


        with patch('timecardsite.views.services') as mocked_services:
            mocked_services.get_access_token.return_value = tokens
            mocked_services.get_access_token.__getitem__.side_effect = tokens.__getitem__
            mocked_services.get_account_info.return_value = account_info
            mocked_services.get_account_info.__getitem__.side_effect = account_info.__getitem__
            self.client.get(f'/auth/?code={code}')

        self.assertEqual(Account.objects.count(), 1)
        new_account = Account.objects.first()
        self.assertEqual(new_account.account_id, account_info['account_id'])
        self.assertEqual(new_account.name, account_info['name'])
        self.assertEqual(new_account.access_token, tokens['access_token'])
        self.assertEqual(new_account.refresh_token, tokens['refresh_token'])