Published on

Functional testing Django applications using Pytest Part 2(Selenium testing)

Welcome back to the 'Unit testing Django applications using Pytest' blog post series!πŸ‘‹In this second part of the series we will cover how to use Selenium to write some browser-based automation unit tests. This is going to be a very informative blog post so just keep reading!πŸ“–

Selenium + Python + Chrome = πŸ’₯πŸ’₯πŸ’₯

Figure 1: Selenium + Python + Chrome = πŸ’₯πŸ’₯πŸ’₯

Setup

Picking up from where we left off from Part 1 of this series, make sure to open the blog application in your favorite coding IDE. If you need to clone the application that we will be working on, here is its GitHub link.

Development

Since our main objective in this blog post is to add some functional tests to our blog application let's do so by creating a func_tests folder in the tests folder of our application. Next, inside the /tests/func_tests/ folder let's create a file named func_test.py. Inside func_test.py let's add the following code:

from django.test import TestCase
from selenium import webdriver
import pytest


class FunctionalTestCase(TestCase):
    LOGIN_URL = 'http://localhost:8000/login/'
    ADMIN_URL = 'http://localhost:8000/admin/login/?next=/admin/'
    @pytest.fixture(autouse=True)
    def setup(self):
        self.browser = webdriver.Firefox()
        yield
        self.tearDown()


    def tearDown(self):
        self.browser.quit()

Now for an explanation of the above code:

  • The FunctionalTestCase class is inherited from the django.test.TestCase class. This is the class that will be used to hold the functional tests that are to be written
  • The setup() method is a fixture that initializes the Selenium webdriver.Firefox() driver. Note that a fixture with the option autouse=True enabled will run that fixture every time a new test case is executed. In this case, setup() will be executed each time a test case is executed. For more info on Pytest Fixtures, visit this link: https://docs.pytest.org/en/6.2.x/fixture.html
  • The tearDown() method is not a fixture but a regular method used to close the browser window that is opened as a result of running the Selenium Firefox Driver

Now that we have a skeleton of the code for the FunctionalTestCase class in use, let's create our first functional test case!

Start by adding the following code to the func_test.py file:

class FunctionalTestCase(TestCase):
    LOGIN_URL = 'http://localhost:8000/login/'
    ADMIN_URL = 'http://localhost:8000/admin/login/?next=/admin/'
    @pytest.fixture(autouse=True)
    def setup(self):
        self.browser = webdriver.Firefox()
        yield
        self.tearDown()

    # Note that testcase methods should always be placed in the middle of fixtures
    @pytest.mark.django_db
    def test_there_is_homepage(self):
        self.browser.get('http://localhost:8000')
        self.assertIn('Blog', self.browser.page_source)

    def tearDown(self):
        self.browser.quit()

The test_there_is_homepage() method is designed to navigate to http://localhost:8000 and search for the word Blog in the browser's page source. If that word is found, the self.assertIn() line would pass the testcase, and if not the testcase would fail. The word 'Blog' is used as an identification criteria because our blogging application mentions the word 'Blog' in its homepage numerous times.

Now let's add a second functional test to the func_test.py file:

    @pytest.mark.django_db
    def test_actions_dropdown_button_works(self):
        self.browser.get('http://localhost:8000')
        self.browser.find_element_by_class_name("dropdown-toggle").click()
        self.assertIn("All Articles", self.browser.page_source)
        all_articles_btn = self.browser.find_element_by_xpath("//a[contains(text(), 'All Articles')]")
        if all_articles_btn.is_displayed():
            assert True
        else:
            pytest.fail()

The test_actions_dropdown_button_works() method is a step up from the test_there_is_homepage() method in that after navigating to http://localhost:8000, it checks for the presence of the text: "All Articles" in the browser's page source.

The 'All Articles' link highlighted in red

Figure 2: The 'All Articles' link highlighted in red

Then, it uses the find_element_by_xpath() method to locate the 'All Articles' button. If that button is visible, which is checked through the is_displayed() method, then the testcase would pass.

Now it's time for the third functional test to the func_test.py file:

    @pytest.mark.django_db
    def test_post_link_works(self):
        self.browser.get('http://localhost:8000')


        old_url = self.browser.current_url
        self.browser.find_element_by_xpath("//a[contains(@href, '/post/2')]").click()
        new_url = self.browser.current_url
        if old_url != new_url:
            assert True
        else:
            pytest.fail()

The test_post_link_works() method is similar to the test_post_link_works() method with the exception of using the find_element_by_xpath() method to locate and click the second post displayed on the homepage of our blogging application. After the post link is click the application should navigate to the following URL: http://localhost:8000/post/2, and in order to validate this we use the current_url attribute of the Selenium Firefox Driver.

Notice how the browser URL changes after clicking the second post link

Figure 3: Notice how the browser URL changes after clicking the second post link

To detect a difference in the browser URL before and after the click event occurs, the old_url and new_url variables are used to stored the values of the before and after URLs, respectively. Finally those variables are compared to each other, in which the outcome of the testcase is dependent on.

Next, we move on to the fourth functional test to the func_test.py file:

class FunctionalTestCase(TestCase):
    LOGIN_URL = 'http://localhost:8000/login/'

    ...
    ...
    ...

    @pytest.mark.django_db
    def test_login_btn_works(self):
        self.browser.get("http://localhost:8000")
        self.browser.find_element_by_xpath("//a[contains(@href, '/login/')]").click()
        login_url = self.browser.current_url
        if login_url == self.LOGIN_URL:
            assert True
        else:
            pytest.fail()

Note: Make sure to add the LOGIN_URL variable near the top of the class because we will need it in the test_login_btn_works() method.

The test_login_btn_works() method is used to click the login button located in the homepage:

Login button highlighted in red

Figure 4: Login button highlighted in red

Once the browser navigates to http://localhost:8000/login/, that URL is compared to the homepage URL to ensure that successful page navigation did occur. If this is the case then the test case would pass via assert True. Alternatively, pytest.fail() would be used to fail the testcase if the URLs do not match.

Last but not least, let's add the fifth functional test to the func_test.py file:



class FunctionalTestCase(TestCase):
    ADMIN_URL = 'http://localhost:8000/admin/login/?next=/admin/'
    ...
    ...
    @pytest.mark.django_db
    def test_admin_link_works(self):
        self.browser.get("http://localhost:8000")
        self.browser.find_element_by_xpath("//a[contains(@href, '/admin/')]").click()
        admin_url = self.browser.current_url
        if admin_url == self.ADMIN_URL:
            assert True
        else:
            pytest.fail()

Note: Make sure to add the ADMIN_URL variable near the top of the class because we will need it in the test_admin_link_works() method.

The test_admin_link_works() method is very similar to the test_login_btn_works() in terms of functionality with the only exception being the clicking of the 'Admin' button as shown here:

Admin button highlighted in red

Figure 5: Admin button highlighted in red

After the 'Admin' button is clicked the URL should changed to the ADMIN_URL, after which the old and new URLs are compared in the previous fashion.

If you made it this far, congrats! You are on track to becoming an expert of writing functional tests using Selenium and PytestπŸš€

Conclusion

Congratulations! You now know how to write basic functional tests using Selenium and Pytest for your Django applications. If you need access to the source code for this application you can access it by visiting it's GitHub link.

Well that's it for this post! Thanks for following along in this article and if you have any questions or concerns please feel free to post a comment in this post and I will get back to you when I find the time.

If you found this article helpful please share it and make sure to follow me on Twitter and GitHub, connect with me on LinkedIn, subscribe to my YouTube channel.