6

I have a view here which adds a new List to the database and redirects to the List page. I have get_absolute_url configured in the model class. It seems to works perfectly.

def new_list(request):
    form = ItemForm(request.POST)

    if form.is_valid():
        list_ = List()
        list_.owner = request.user
        list_.save()
        form.save(for_list=list_)
        return redirect(list_)
    else:
        return render(request, 'home.html', {'form': form})

But the problem happens when I try to mock the model class and the form class with patch from unitest.mock

class TestMyLists(TestCase):

    @patch('lists.views.List')
    @patch('lists.views.ItemForm')
    def test_list_owner_is_saved_if_user_is_authenticated(
        self, mockItemFormClass, mockListClass
    ):
        user = User.objects.create(email='a@b.com')
        self.client.force_login(user)
        self.client.post('/lists/new', data={'text': 'new item'})
        mock_list = mockListClass.return_value
        self.assertEqual(mock_list.owner, user)

When I run the test, I get error like this:

Traceback (most recent call last):
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/core/handlers/exception.py", line 35, in inner
  response = get_response(request)
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/core/handlers/base.py", line 128, in _get_response
  response = self.process_exception_by_middleware(e, request)
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/core/handlers/base.py", line 126, in _get_response
  response = wrapped_callback(request, *callback_args, **callback_kwargs)
File "/mnt/BAC4BB93C4BB4FFD/codes/tdd/superlists/lists/views.py", line 36, in new_list
  return redirect(list_)
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/shortcuts.py", line 58, in redirect
  return redirect_class(resolve_url(to, *args, **kwargs))
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/http/response.py", line 407, in __init__
  self['Location'] = iri_to_uri(redirect_to)
File "/home/sjsakib/.virtualenvs/superlists/lib/python3.6/site-packages/django/utils/encoding.py", line 151, in iri_to_uri
  return quote(iri, safe="/#%[]=:;$&()+,!?*@'~")
File "/usr/local/lib/python3.6/urllib/parse.py", line 787, in quote
  return quote_from_bytes(string, safe)
File "/usr/local/lib/python3.6/urllib/parse.py", line 812, in quote_from_bytes
  raise TypeError("quote_from_bytes() expected bytes")
TypeError: quote_from_bytes() expected bytes

It seems like the redirect function will not work with a mock object. How can I fix this? I'm using Django 2.0.1

Kryštof Řeháček
  • 1,965
  • 1
  • 16
  • 28
S.j. Sakib
  • 426
  • 1
  • 4
  • 16
  • 1
    How are you expecting it to work? Where are you expecting it to redirect to? Django knows to use a model instance's `get_absolute_url` method for a redirect, but what is it supposed to do with your mock object? – Daniel Roseman Feb 11 '18 at 20:37
  • I expect it to return a `HttpResponseRedirect` object. Of course it doesn't go anywhere, but the unittest works. It works that way in earlier versions of Django. I can achieve the same result using `return redirect(str(list_.get_absolute_url()))` in the view. That's what I'm doing currently. But the code doesn't look look good. – S.j. Sakib Feb 13 '18 at 03:18

2 Answers2

2

I'm learning the same tutorial, and i got the same error, however i find the solution here: Mock() function gives TypeError in django2

The cause is:

Django 2 doesn't support anymore bytestrings in some places so when the views redirect the mock Class List it does as a mock object and the iri_to_uri django function throws an error.

In django 1.11 iri_to_uri forced the iri to a bytes return quote(force_bytes(iri), safe="/#%[]=:;$&()+,!?@'~") instead now is return quote(iri, safe="/#%[]=:;$&()+,!?@'~").

So the solution is to return redirect(str(list_.get_absolute_url())) instead of return redirect(list_) in the lists.views.py

Here is my example:

def new_list(request):
    form = ItemForm(data=request.POST)
    if form.is_valid():
        list_ = List()
        list_.owner = request.user
        list_.save()
        form.save(for_list=list_)
        return redirect(str(list_.get_absolute_url()))
    else:
        return render(request, 'home.html', {"form": form})
hj24
  • 99
  • 6
2

Please see my answer here which only changes the testing code, while resulting in the desired production code.

In short,

  1. Patch the redirect
@patch('app_name.views.redirect')
  1. Pass mock_redirect as argument
def test_new_list_redirects_view(self, mock_redirect):
  1. Check that the response is equal to the return_value of the mock_redirect
response = new_list(self.request)
self.assertEqual(response, mock_redirect.return_value)
Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
Raoul
  • 1,876
  • 1
  • 14
  • 14
  • 1
    Raoul interesting take but [I have ended up going with another approach](https://stackoverflow.com/a/74436201/5675325) (+1 though for considering not making the other changes to the view function). – Tiago Martins Peres Nov 14 '22 at 18:27