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='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)

My full traceback:

TypeError: quote_from_bytes() expected bytes

traceback

What can be?

thank you

Tiago Martins Peres
  • 14,289
  • 18
  • 86
  • 145
fabio
  • 1,210
  • 2
  • 26
  • 55

2 Answers2

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

fabio
  • 1,210
  • 2
  • 26
  • 55
  • 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. – Bobort Apr 01 '20 at 21:47
  • This solution is simple enough to unblock people using latest Django for the tutorial. love it! – Yeonghun Mar 24 '22 at 08:53
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.
Raoul
  • 1,876
  • 1
  • 14
  • 14