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)
Вернуться на верх