Quick start on Selenium tests with Django and GitHub Actions deployment
Writing a combination of unit and integration/UI tests is ideal for any kind of project that contains both user-facing components and business logic.
Django has excellent support for both of these kinds of tests. Unit tests are more straightforward to write and deploy in a CI/CD pipeline. The challenge in most languages and platforms comes when you need to write Integration/UI tests. Most of the time these integration tests are slow to write, slow to execute, and many times unreliable and flaky when you run them in a CI/CD system. In Django, these tests are supported via the popular Selenium framework used for testing all kinds of web applications.
Django gives you a great headstart when writing integration tests by coupling together a local running server with your app and a local DB and Selenium WebDriver. In this post, I will quickly guide you through a simple Django test using Selenium and how to deploy it to GitHub Actions for running it every time there's a new PR or commit.
The test part
Selenium WebDriver offers a great API for interacting with a browser and "driving" it to perform clicks and interactions to test your web app.
You can write most of these tests manually, but if you are new to the space I would strongly recommend giving Selenium IDE a try. This is a browser extension to quickly start with Selenium WebDriver tests without writing a single line of code. As your tests and app become complex over time, you would most probably need to write code, but still, the tests created by Selenium IDE can be a great starting point (you export those tests and modify the code).
Below is a snippet of a simple integration test in Django. A reminder that you would need to have the selenium
installed and a web browser of your choice to run it locally.
class LoginFlowTests(StaticLiveServerTestCase): # 1.
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.test_user = create_test_user()
cls.selenium = WebDriver() # 2.
cls.selenium.implicitly_wait(5) # 3.
@classmethod
def tearDownClass(cls):
cls.selenium.quit()
super().tearDownClass()
def test_login_flow(self):
test.selenium.get(f"{test.live_server_url}/accounts/login") # 4.
username_input = test.selenium.find_element(By.NAME, "user") # 5.
username_input.send_keys(test.test_user.email) # 6.
password_input = test.selenium.find_element(By.NAME, "pass")
password_input.send_keys(test.test_user.password)
test.selenium.find_element(By.ID, "sign-in").click()
self.assertEqual(user_has_logged_in(self.test_user), true) # 7.
- The
StaticLiveServerTestCase
superclass is provided by Django and spins up a local running server with your app and a clean local database. Notice theStatic
prefix; this local server will serve static files as well, without the need to runcollectstatic
first (just like in development). - The
WebDriver
class is implemented for each browser that you want to "drive". For instance, if you would like to "drive" the Chrome browser, you would need to add:from selenium.webdriver.chrome.webdriver import WebDriver
in your imports section. - This is an initial wait until our app is ready before starting to interact with it. Unfortunately, these non-deterministic "waits" are common among integration tests due to the "async" nature of web apps today. For instance, the browser might report back that the page has been loaded, but there might be an async JS script that has not yet finished.
- This tells WebDriver to navigate to the login URL. The
test.live_server_url
variable holds the location of the spinned-up local instance of your app. - There are multiple ways to select elements on the screen. Checkout the full documentation on how exactly you can find elements in the screen (e.g. by the
name
attribute, by theid
attribute, using a CSSclass
selector, etc). - After you find an element on the screen, you can interact with that element, like writing text on an input box or clicking it.
- Finally, you would need to make some assertions to make sure that what you are seeing on screen is what you are expecting. This can be done on screen elements (i.e. find the elements and check for one of their attributes) or check the persistent storage (i.e. the database) that the change you are expecting has been made (or ideally, both).
The CI part
Running these integration tests every time there's a significant change in the code base (e.g. pull request, merge, etc) is part of the Continuous Integration matra. GitHub Actions offer a robust ecosystem where you can set up these kinds of CIs quickly.
What I did was combine the official setup-python Action with the setup-chromedriver Action to set up an environment where a Django Selenium WebDriver test can run. Below is the script I am using; feel free to adapt it to your needs.
Hopefully, this was a quick and easy primer into Django integration tests using Selenium WebDriver.
Happy testing!