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!📖
Figure 1: Selenium + Python + Chrome = 💥💥💥
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.
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 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:
FunctionalTestCaseclass is inherited from the
django.test.TestCaseclass. This is the class that will be used to hold the functional tests that are to be written
setup()method is a fixture that initializes the Selenium
webdriver.Firefox()driver. Note that a fixture with the option
autouse=Trueenabled 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
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
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()
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
@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()
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.
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
@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()
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.
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
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
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 is used to click the login button located in the homepage:
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
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 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:
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🚀
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.