Search code examples
pythondjangopytestdjango-testingpytest-django

pytest-django: Is this the right way to test view with parameters?


Say I'm testing an RSS feed view in a Django app, is this how I should go about it?

def test_some_view(...):
    ...
    requested_url = reverse("personal_feed", args=[some_profile.auth_token])
    resp = client.get(requested_url, follow=True)
    ...
    assert dummy_object.title in str(resp.content)
  1. Is reverse-ing and then passing that into the client.get() the right way to test? I thought it's DRYer and more future-proof than simply .get()ing the URL.

  2. Should I assert that dummy_object is in the response this way?

  3. I'm testing here using the str representation of the response object. When is it a good practice to do this vs. using selenium? I know it makes it easier to verify that said obj or property (like dummy_object.title) is encapsulated within an H1 tag for example. On the other hand, if I don't care about how the obj is represented, it's faster to do it like the above.


Solution

  • Reevaluating my comment (didn't carefully read the question and overlooked the RSS feed stuff):

    1. Is reverse-ing and then passing that into the client.get() the right way to test? I thought it's DRYer and more future-proof than simply .get()ing the URL.

    I would agree on that - from Django point, you are testing your views and don't care about what the exact endpoints they are mapped against. Using reverse is thus IMO the clear and correct approach.

    1. Should I assert that dummy_object is in the response this way?

    You have to pay attention here. response.content is a bytestring, so asserting dummy_object.title in str(resp.content) is dangerous. Consider the following example:

    from django.contrib.syndication.views import Feed
    
    class MyFeed(Feed):
        title = 'äöüß'
        ...
    

    Registered the feed in urls:

    urlpatterns = [
        path('my-feed/', MyFeed(), name='my-feed'),
    ]
    

    Tests:

    @pytest.mark.django_db
    def test_feed_failing(client):
        uri = reverse('news-feed')
        resp = client.get(uri)
        assert 'äöüß' in str(resp.content)
    
    
    @pytest.mark.django_db
    def test_feed_passing(client):
        uri = reverse('news-feed')
        resp = client.get(uri)
        content = resp.content.decode(resp.charset)
        assert 'äöüß' in content
    

    One will fail, the other won't because of the correct encoding handling.

    As for the check itself, personally I always prefer parsing the content to some meaningful data structure instead of working with raw string even for simple tests. For example, if you are checking for data in a text/html response, it's not much more overhead in writing

    soup = bs4.BeautifulSoup(content, 'html.parser')
    assert soup.select_one('h1#title-headliner') == '<h1>title</h1>'
    

    or

    root = lxml.etree.parse(io.StringIO(content), lxml.etree.HTMLParser())
    assert next(root.xpath('//h1[@id='title-headliner']')).text == 'title'
    

    than just

    assert 'title' in content
    

    However, invoking a parser is more explicit (you won't accidentally test for e.g. the title in page metadata in head) and also makes an implicit check for data integrity (e.g. you know that the payload is indeed valid HTML because parsed successfully).

    To your example: in case of RSS feed, I'd simply use the XML parser:

    from lxml import etree
    
    def test_feed_title(client):
        uri = reverse('my-feed')
        resp = client.get(uri)
        root = etree.parse(io.BytesIO(resp.content))
        title = root.xpath('//channel/title')[0].text
        assert title == 'my title'
    

    Here, I'm using lxml which is a faster impl of stdlib's xml. The advantage of parsing the content to an XML tree is also that the parser reads from bytestrings, taking care about the encoding handling - so you don't have to decode anything yourself.

    Or use something high-level like atoma that ahs a nice API specifically for RSS entities, so you don't have to fight with XPath selectors:

    import atoma
    
    @pytest.mark.django_db
    def test_feed_title(client):
        uri = reverse('my-feed')
        resp = client.get(uri)
        feed = atoma.parse_atom_bytes(resp.content)
        assert feed.title.value == 'my title'
    

    1. ...When is it a good practice to do this vs. using selenium?

    Short answer - you don't need it. I havent't paid much attention when reading your question and had HTML pages in mind when writing the comment. Regarding this selenium remark - this library handles all the low-level stuff, so when the tests start to accumulate in count (and usually, they do pretty fast), writing

    uri = reverse('news-feed')
    resp = client.get(uri)
    root = parser.parse(resp.content)
    assert root.query('some-query')
    

    and dragging the imports along becomes too much hassle, so selenium can replace it with

    driver = WebDriver()
    driver.get(uri)
    assert driver.find_element_by_id('my-element').text == 'my value'
    

    Sure, testing with an automated browser instance has other advantages like seeing exactly what the user would see in real browser, allowing the pages to execute client-side javascript etc. But of course, all of this applies mainly to HTML pages testing; in case of testing against the RSS feed selenium usage is an overkill and Django's testing tools are more than enough.