Freelancing for Pale Blue

Looking for flexible work opportunities that fit your schedule?

Quick start on Selenium tests with Django and GitHub Actions deployment

Django Feb 11, 2023

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.

    def setUpClass(cls):
        cls.test_user = create_test_user()
        cls.selenium = WebDriver() # 2.
        cls.selenium.implicitly_wait(5) # 3. 

    def tearDownClass(cls):

    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( # 6.
        password_input = test.selenium.find_element(By.NAME, "pass")
        test.selenium.find_element(By.ID, "sign-in").click()
        self.assertEqual(user_has_logged_in(self.test_user), true) # 7.
  1. The StaticLiveServerTestCase superclass is provided by Django and spins up a local running server with your app and a clean local database. Notice the Static prefix; this local server will serve static files as well, without the need to run collectstatic first (just like in development).
  2. 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 import WebDriver in your imports section.
  3. 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.
  4. 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.
  5. 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 the id attribute, using a CSS class selector, etc).
  6. After you find an element on the screen, you can interact with that element, like writing text on an input box or clicking it.
  7. 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.

name: Integration tests

    branches: [ "main" ]
    branches: [ "main" ]


    runs-on: ubuntu-latest
      max-parallel: 4
        python-version: [3.11]

    - uses: actions/checkout@v3
    - uses: nanasess/setup-chromedriver@v1
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v3
        python-version: ${{ matrix.python-version }}
    - name: Install Dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
    - name: Run Tests
      run: |
        export DISPLAY=:99
        chromedriver --url-base=/wd/hub &
        sudo Xvfb -ac :99 -screen 0 1280x1024x24 > /dev/null 2>&1 & # optional
        cd src
        python test web 

Hopefully, this was a quick and easy primer into Django integration tests using Selenium WebDriver.

Happy testing!


Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.