Hello again,
A couple weeks ago I wanted to test the templates of a site I’m building, mainly javascript functions and some user interactions, and decided to use selenium.
I wanted to use raw selenium because that’s my first time using it in python, so I wanted to learn how it works without the help of third-party modules.
To my surprise, I didn’t find a straight forward tutorial, so I’m writing one with the steps I did to make it work. The tests I created are available at the end of this post.
It’s worth noting that I used selenium 3.1, django 2.1 and python 3.6. Running the server on Ubuntu 18.
Let’s begin!
1. Installation
The first step is to install selenium. I used pip for that:
pip3 install selenium
Selenium requires a driver to launch the browser, and each browser has its own. Firefox, the one I chose, uses the geckodriver, which is available here.
The driver must be in a folder listed in the PATH environment variable. In Ubuntu, just move it to the /bin folder and it’s ready to use.
2. StaticLiveServerTestCase
Selenium demands the test class to be either a LiveServerTestCase or a StaticLiveServerTestCase, that’s because it needs the server running to test the site.
Both classes are similar, the difference is that the latter will load the static content (custom css and javascript files for instace) while the former won’t.
I prefer StaticLiveServerTestCase because one of the reasons for using selenium is to test the javascript functions, so, static content is necessary.
3. Class Structure
The main methods to consider are:
- setUpClass(): executed once before the first test.
- tearDownClass(): executed once after the last test.
- setUp(): executed before each test.
- tearDown(): executed after each test.
The setUpTestData() isn’t on the list because it isn’t available in LiveServerTestCase.
A good place to open the browser is in the setUpClass() because it takes long to open it, this way it’ll use the same window in all the tests. What leads to using tearDownClass() to close the browser.
In a LiveServerTestCase the database is flushed after each test, then it’s necessary to populate it before each test too. Either setUp() or the own test are good places for it, although setUp() is the expected place and should be favored.
tearDown() isn’t really necessary for the base structure but it’s good to know that it exists :3
The base test class should look like:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.contrib.staticfiles.testing import StaticLiveServerTestCase | |
from selenium import webdriver | |
class TestName(StaticLiveServerTestCase): | |
@classmethod | |
def setUpClass(cls): | |
super().setUpClass() | |
cls.browser = webdriver.Firefox() | |
@classmethod | |
def tearDownClass(cls): | |
cls.browser.quit() | |
super().tearDownClass() | |
def setUp(self): | |
super(TestName, self).setUp() | |
# Populate the database here |
4. live_server_url
To access a webpage in selenium we must use the URL of the testing server. The URL changes everytime time you run the tests, a different port is assigned to the server, but it’s stored in the variable live_server_url. An webpage can be accessed like this:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
def test_create_button(self): | |
self.browser.get(self.live_server_url + 'question/create/') | |
# Test the page |
5. Explicit and Implicit Waits
As far as I know selenium waits the page to be ready before executing commands. However, in some cases we will need to wait some action to complete so we can resume testing the page.
Usually this happens when a javascript function is used, and we must wait its completion, or when the test case loads another page and we should wait for it, otherwise the next comands will run in the initial page and fail.
In those cases we can use both implicit or explicit waits. The implicit one simply stops the execution for a fixed amount of time, you can use it as in:
Usually, implicit waits are a bad choice because you’ll risk either having unnecessarily long wait times or inconsistent tests.
Explicit waits, on the other hand, are more reliable. They’ll wait for a given expected condition to happen and resume as soon as it does. The following example shows how to wait for an element to be clickable:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from django.urls import reverse | |
from selenium.webdriver.support.ui import WebDriverWait | |
from selenium.webdriver.support import expected_conditions as EC | |
from selenium.webdriver.common.by import By | |
def test_title_redirects_to_details(self): | |
self.browser.get(self.live_server_url + reverse('question-list')) | |
title = WebDriverWait(self.browser, 5).until( | |
EC.element_to_be_clickable((By.CSS_SELECTOR, '#question2 .card-link-title')) | |
) |
There are multiple builtin expected conditions which you can find here. Notice that some conditions expect a locator, like element_to_be_clickable(locator) in the snippet above, in this case you should use the By class. You can find all the available locators here, the usage of them all is similar to By.CSS_SELECTOR.
6. Login user in code
In some tests it might be useful to login the user in code. The other option is using selenium to simulate the actual login, which will take longer. In case you are testing a page that requires the login instead of the login page, it’s better to save this time.
The trick I found to login the user is to use cookies to pass a logged in session to the browser. You can set the cookie in the setUp() like:
This trick will make the slow tests a little less slow 🙂
7. Auto ids
At last, I’d like to warn you about creating objects in the database when the model uses auto generated ids. It happens that the database is flushed between tests but the auto id counter won’t necessarily reset.
Then, if you add two objects to the database, their ids will be 1 and 2 in the first test and 3 and 4 in the second. This behavior might break some tests which use the id to identify the object. In this case, always set the id yourself, like:
8. Example
With everything stated above you should be ready to test your project with flexibility. I’ll also leave the tests I created here as reference, hope they help 🙂