Tutorial Part 4: Automated Testing
4 minute read
This tutorial begins where Tutorial 3 left off. We’ve built a simple chat server and now we’ll create some automated tests for it.
Testing the views
To ensure that the chat server keeps working, we will write some tests.
We will write a suite of end-to-end tests using Selenium to control a Chrome web browser. These tests will ensure that:
- when a chat message is posted then it is seen by everyone in the same room
- when a chat message is posted then it is not seen by anyone in a different room
Install the Chrome web browser, if you do not already have it.
Install Selenium as a development dependency, like the testing tools in the MDN tutorial, so that it is never installed on your production server. Run the following command:
uv add --dev selenium
Commit the updated pyproject.toml and uv.lock.
You don’t need to install chromedriver yourself. The first time the tests start Chrome, Selenium finds a matching chromedriver or downloads one automatically.
Create a new file chat/tests.py. Your app directory should now look like:
chat/
__init__.py
consumers.py
routing.py
templates/
chat/
index.html
room.html
tests.py
urls.py
views.py
Put the following code in chat/tests.py:
# chat/tests.py
from channels.testing import ChannelsLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.wait import WebDriverWait
class ChatTests(ChannelsLiveServerTestCase):
serve_static = True # emulate StaticLiveServerTestCase
@classmethod
def setUpClass(cls):
super().setUpClass()
try:
# NOTE: Selenium finds or downloads a matching chromedriver automatically
cls.driver = webdriver.Chrome()
except:
super().tearDownClass()
raise
@classmethod
def tearDownClass(cls):
cls.driver.quit()
super().tearDownClass()
def test_when_chat_message_posted_then_seen_by_everyone_in_same_room(self):
try:
self._enter_chat_room("room_1")
self._open_new_window()
self._enter_chat_room("room_1")
self._switch_to_window(0)
self._post_message("hello")
WebDriverWait(self.driver, 2).until(
lambda _: "hello" in self._chat_log_value,
"Message was not received by window 1 from window 1",
)
self._switch_to_window(1)
WebDriverWait(self.driver, 2).until(
lambda _: "hello" in self._chat_log_value,
"Message was not received by window 2 from window 1",
)
finally:
self._close_all_new_windows()
def test_when_chat_message_posted_then_not_seen_by_anyone_in_different_room(self):
try:
self._enter_chat_room("room_1")
self._open_new_window()
self._enter_chat_room("room_2")
self._switch_to_window(0)
self._post_message("hello")
WebDriverWait(self.driver, 2).until(
lambda _: "hello" in self._chat_log_value,
"Message was not received by window 1 from window 1",
)
self._switch_to_window(1)
self._post_message("world")
WebDriverWait(self.driver, 2).until(
lambda _: "world" in self._chat_log_value,
"Message was not received by window 2 from window 2",
)
self.assertTrue(
"hello" not in self._chat_log_value,
"Message was improperly received by window 2 from window 1",
)
finally:
self._close_all_new_windows()
# === Utility ===
def _enter_chat_room(self, room_name):
self.driver.get(self.live_server_url + "/chat/")
ActionChains(self.driver).send_keys(room_name, Keys.ENTER).perform()
WebDriverWait(self.driver, 2).until(
lambda _: room_name in self.driver.current_url
)
def _open_new_window(self):
self.driver.execute_script('window.open("about:blank", "_blank");')
self._switch_to_window(-1)
def _close_all_new_windows(self):
while len(self.driver.window_handles) > 1:
self._switch_to_window(-1)
self.driver.execute_script("window.close();")
if len(self.driver.window_handles) == 1:
self._switch_to_window(0)
def _switch_to_window(self, window_index):
self.driver.switch_to.window(self.driver.window_handles[window_index])
def _post_message(self, message):
ActionChains(self.driver).send_keys(message, Keys.ENTER).perform()
@property
def _chat_log_value(self):
return self.driver.find_element(
by=By.CSS_SELECTOR, value="#chat-log"
).get_property("value")
Our test suite extends ChannelsLiveServerTestCase rather than Django’s usual
suites for end-to-end tests (StaticLiveServerTestCase or LiveServerTestCase) so
that URLs inside the Channels routing configuration like /ws/chat/ROOM_NAME/
will work inside the suite.
We are using sqlite3, which for testing is run as an in-memory database, and therefore the
tests will not run correctly. We need to tell our project that the test database should be a
file instead. Edit the DATABASES setting in locallibrary_config/settings.py and add a
TEST entry, keeping the ENGINE and NAME lines you already have:
# locallibrary_config/settings.py
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
'TEST': {
'NAME': BASE_DIR / 'test_db.sqlite3',
},
}
}
The original tutorial sets the test NAME to db.sqlite3, the same file as your development
database. Django deletes the test database when the tests finish, so that setting would
delete your development database: your superuser, and every book, author, and genre you’ve
created. Make sure the TEST name above is test_db.sqlite3.
Django creates test_db.sqlite3 while the tests run, and it should never be committed. If you
started your .gitignore from GitHub’s Python template, its *.sqlite3 line already ignores
it. Otherwise, add this line:
test_db.sqlite3
The tests use your real channel layer, so Redis must be running (see Part 2). To run the tests, run the following command:
uv run python manage.py test chat.tests
You should see output that looks like:
Creating test database for alias 'default'...
Found 2 test(s).
System check identified no issues (0 silenced).
Not Found: /favicon.ico
..
----------------------------------------------------------------------
Ran 2 tests in 8.953s
OK
Destroying test database for alias 'default'...
Chrome windows will open and close by themselves while the tests run. The Not Found: /favicon.ico line is Chrome asking for an icon the chat pages don’t have; you can ignore it.
You now have a tested chat server!
What’s next?
Congratulations! You’ve fully implemented a chat server, made it performant by writing it in asynchronous style, and written automated tests to ensure it won’t break.
This is the end of the tutorial. At this point you should know enough to start an app of your own that uses Channels and start fooling around. As you need to learn new tricks, come back to the rest of the Channels documentation.