Mock() function gives TypeError in django2
Asked Answered
S

2

4

I'm following this tutorial.

When I run test_views.py I have an error that shouldn't be there according the author: TypeError: quote_from_bytes() expected bytes.

My views and my test_views are the same like the book, but I'm using django 2.0.6 instead django 1.11 so my url.py change, so maybe here's the problem.

Edit:

at a second look the problem appears to be in the mock() function.

When I use patch('lists.views.List') the Print(list_) in my view gives <MagicMock name='List()' id='79765800'> instead of List object (1)

/edit

My lists/urls.py:

urlpatterns = [
    path('new', views.new_list, name='new_list'),
    path('<slug:list_id>/',
        views.view_list, name='view_list'),
    path('users/<email>/',         # I'm not sure about this one but it works in other tests
        views.my_lists, name='my_lists'),
]
#instead of:
#urlpatterns = [
#    url(r'^new$', views.new_list, name='new_list'),
#    url(r'^(\d+)/$', views.view_list, name='view_list'),
#    url(r'^users/(.+)/$', views.my_lists, name='my_lists'),
#]

My lists/views.py:

[...]
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_)
        Print(list_)
        return redirect(list_)
    else:
        return render(request, 'home.html', {"form": form})

My lists/tests/test_views.py:

@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='[email protected]')
    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)

My full traceback:

TypeError: quote_from_bytes() expected bytes

traceback

What can be?

thank you

Stempson answered 20/6, 2018 at 19:59 Comment(0)
S
5

At last I found the solution on-line.

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

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(list_)
        return redirect(str(list_.get_absolute_url()))
    else:
        return render(request, 'home.html', {"form": form})

I hope this helps someone else

Stempson answered 21/6, 2018 at 16:57 Comment(2)
Oh, yes, absolutely helpful! Thank you so much! I'm using Django 3. But I don't understand why the problem only shows up with using the mock library. Without it, redirect(list_) works just fine. There must be more to this.Speedwell
This solution is simple enough to unblock people using latest Django for the tutorial. love it!Marya
Z
4

I've solved this in the testing code without changing the desired production code as follows:

@patch('lists.views.NewListForm')
class NewListViewUnitTest(unittest.TestCase):
    def setUp(self):
        self.request = HttpRequest()
        self.request.POST['text'] = 'new list item'
        self.request.user = Mock()

def test_passes_POST_data_to_NewListForm(self, mockNewListForm):
    mock_form = mockNewListForm.return_value
    returned_object = mock_form.save.return_value
    returned_object.get_absolute_url.return_value = 'fakeurl'

    new_list2(self.request)

    mockNewListForm.assert_called_once_with(data=self.request.POST)

def test_saves_form_with_owner_if_form_valid(self, mockNewListForm):
    mock_form = mockNewListForm.return_value
    mock_form.is_valid.return_value = True
    returned_object = mock_form.save.return_value
    returned_object.get_absolute_url.return_value = 'fakeurl'

    new_list2(self.request)

    mock_form.save.assert_called_once_with(owner=self.request.user)

@patch('lists.views.redirect')
def test_redirects_to_form_returned_object_if_form_valid(
    self, mock_redirect, mockNewListForm
):
    mock_form = mockNewListForm.return_value
    mock_form.is_valid.return_value = True

    response = new_list2(self.request)

    self.assertEqual(response, mock_redirect.return_value)
    mock_redirect.assert_called_once_with(mock_form.save.return_value)

Note that assigning some_method.return_value sets the response of some_method, without calling some_method(), so we can also test that the method was only called once.

What I like about this solution is that it results in the desired production code:

def new_list2(request):
    form = NewListForm(data=request.POST)
    list_ = form.save(owner=request.user)
    return redirect(list_)

.. instead of using a workaround in the production code like return redirect(str(list_.get_absolute_url())), which is not desirable because it:

  1. Is not the desired production code
  2. Is less elegant production code
  3. Just returns the name of the mocked object as a string (i.e. <MagicMock name='NewListForm().save().get_absolute_url()' id='4363470544'>), which is not what we want: we want to call the get_absolute_url() method and that method (not str()) should return a url as a string.
Zayin answered 25/3, 2021 at 12:54 Comment(2)
This should be the accepted answer. It is far more elegant and does not interfere with the code base at all. Works wiith django 4.04Studdingsail
It is more elegant, but it doesn't refer to the code from the question, which leaves those of us unclear on mocks still at sea.Dowlen

© 2022 - 2024 — McMap. All rights reserved.