Selenium doesn't use testing database during Django functional tests in Docker or cannot access container because of port issues
Issue
My Selenium Chrome Browser cannot access my Django web application when it uses Djangos test database. Djangos StaticLiveServerTestCase
creates random ports when creating test databases. With docker I can only expose hard coded ports:
host_ip = socket.gethostbyname(socket.gethostname())
host_port = urlparse(self.live_server_url).port
self.live_server_url = f"http://{host_ip}:{host_port}"
# results in
# selenium.common.exceptions.WebDriverException: Message: unknown error: net::ERR_CONNECTION_REFUSED
I can set one fixed port on my tests. But then they complain, that the port is already taken.
class FunctionalTest(StaticLiveServerTestCase):
"""Functional Testing base class."""
port = 50500
# resulsts in:
# OSError: [Errno 98] Address in use
The other option I thought of was to set the URL for Selenium to my running web application, but than the test database created by StaticLiveServerTestCase
won't be used. Instead it uses the postgres container, where objects created in the setup phase cannot be found. In my example test the user cannot login, because the Selenium access a version of the application serving the postgres db instead of the testing db.
self.live_server_url = "http://web:8000"
# results in
# AssertionError: 'Profile' not found in 'Sign In'
# the surrounding code is setup correctly, so this normally results in success.
Setup
I try to create a pipeline for my test driven development. For this I chose Django for the web application, postgres as database (development & production) and I use Selenium for my functional tests. All of this happens in docker containers provisioned by docker compose. When I push my code to my self hosted Gitlab a pipeline builds the container and runs all tests. The error also raises on my development machine.
I test like this: docker compose up -d docker compose exec web python3 manage.py test
This is my first time setting something like this up, so maybe I just confuse different concepts.
Code
docker-compose.yml
services:
web:
build: .
develop:
watch:
- action: sync
path: ./src
target: /src
- action: rebuild
path: requirements.txt
ports:
- 8000:8000
environment:
- SECRET_KEY='django-insecure-5&!h2y%p8)l1than6$$jsl$$jb^*7sp+446b7gjfd_fu8=tx_=&9'
- DEBUG=1
- DB_HOST=db
- DB_NAME=devdb
- DB_USER=devuser
- DB_PASS=changeme
- DJANGO_SUPERUSER_PASSWORD=testpass123
- DJANGO_SUPERUSER_USERNAME=admin
- DJANGO_SUPERUSER_EMAIL=admin@mail.com
- ALLOWED_HOSTS=*
depends_on:
db:
condition: service_healthy
command: >
sh -c "python manage.py wait_for_db &&
python manage.py migrate &&
python manage.py createsuperuser --noinput &&
python manage.py runserver 0.0.0.0:8000"
db:
image: postgres:13-alpine
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
ports:
- 5432:5432
healthcheck:
test: ["CMD-SHELL", "pg_isready", "-d", "db_prod"]
interval: 30s
timeout: 60s
retries: 5
start_period: 80s
selenium-chrome:
image: selenium/standalone-chrome:131.0
ports:
- 4444:4444
- 5900:5900
- 7900:7900
test.py
"""Base functional testing class."""
import os
import socket
import uuid
from urllib.parse import urlparse
from django.contrib.auth import get_user_model
from django.contrib.staticfiles.testing import StaticLiveServerTestCase
from selenium import webdriver
from selenium.webdriver.common.by import By
class FunctionalTest(StaticLiveServerTestCase):
"""Functional Testing base class."""
def setUp(self):
"""Setup Selenium and test database access."""
super().setUpClass()
host_ip = socket.gethostbyname(socket.gethostname())
host_port = urlparse(self.live_server_url).port
# Selenium cannot access the website because the random port is not open.
self.live_server_url = f"http://{host_ip}:{host_port}"
# # Selenium can access the web site with this
# # but Django does not use the testing database created by testcase
# self.live_server_url = "http://web:8000"
options = webdriver.ChromeOptions()
self.browser = webdriver.Remote(
command_executor="http://selenium-chrome:4444/wd/hub",
options=options
)
def tearDown(self):
"""Shutdown browser."""
self.browser.quit()
super().tearDown()
def create_test_user(self):
"""Creates a test user."""
User = get_user_model()
self.normal_user_credentials = {
"username": f"normal_user{str(uuid.uuid4())}",
"password": "testpass1234",
"email": "normal_user@email.com"
}
self.normal_user = User.objects.create_user(
username=self.normal_user_credentials["username"],
email=self.normal_user_credentials["email"],
password=self.normal_user_credentials["password"]
)
self.normal_user_credentials["id"] = self.normal_user.id
def test_login(self):
"""Login as existing user."""
self.browser.get(f"{self.live_server_url}/accounts/login/")
# Find input fields
username_input = self.browser.find_element(self, By.NAME,"login")
password_input = self.browser.find_element(self, By.NAME,"password")
# Fill out form
username_input.send_keys(self.normal_user_credentials["username"])
password_input.send_keys(self.normal_user_credentials["password"])
# Submit form
self.browser.find_element(self, By.XPATH, "//button[@type='submit']").click()
header_text = self.browser.find_element(self, By.TAG_NAME, "h1").text
# Check if login was successful
self.assertIn("Profile", header_text)