Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .dockerignore
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Для информации (исправлять не нужно) - для разных образов (точнее Dockerfile'ов) можно указывать свои .dockerignore (ссылка)

Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,3 @@ venv
__pycache__
Dockerfile*
app/playground
tests/selenium
7 changes: 4 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ jobs:

- name: Build docker-compose
run: |
docker compose build
docker compose -f docker-compose.yml -f docker-compose-selenium.yml build

- name: Run docker-compose
run:
APP_CONF=../app_conf/testing.ini docker compose up -d
APP_CONF=../app_conf/testing.ini docker compose -f docker-compose.yml -f docker-compose-selenium.yml up -d

- name: Run tests
run: |
docker ps -a
docker compose logs
docker exec web_speech_trainer-web-1 bash -c 'cd /project/tests && pytest .'
docker exec web_speech_trainer-web-1 bash -c 'cd /project/tests && pytest --ignore=selenium'
docker exec web_speech_trainer-selenium-tests-1 bash -c 'pytest .'
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
.venv
venv
.venv
.idea
ssl
__pycache__
Expand Down
22 changes: 22 additions & 0 deletions Dockerfile_selenium
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM selenium/standalone-chrome:137.0-chromedriver-137.0-grid-4.33.0-20250606

WORKDIR /usr/src/project

USER root
RUN apt-get update && \
apt-get install -y python3 python3-pip && \
rm -rf /var/lib/apt/lists/*

COPY tests/requirements.txt requirements.txt
RUN pip install -r requirements.txt

COPY tests/selenium selenium
COPY tests/test_data test_data
COPY tests/simple_phrases_russian.wav simple_phrases_russian.wav

COPY app/config.py app/config.py
COPY app_conf/testing.ini app_conf/testing.ini

ENV PYTHONPATH='/usr/src/project/:/usr/src/project/app/'

# CMD ["pytest", "-s", "."]
17 changes: 0 additions & 17 deletions Dockerfile_test

This file was deleted.

1 change: 1 addition & 0 deletions app/routes/lti.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def lti():
an empty dictionary with 404 HTTP return code if access was denied.
"""
params = request.form

consumer_key = params.get('oauth_consumer_key', '')
consumer_secret = ConsumersDBManager().get_secret(consumer_key)
request_info = dict(
Expand Down
6 changes: 3 additions & 3 deletions app/static/js/recording.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ let gumStream,
timer;

function startRecording() {
console.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia")
console.log(navigator)
console.log(navigator.mediaDevices)
console.log("call startRecording(). Try to call navigator.mediaDevices.getUserMedia");
console.log("navigator", navigator);
console.log("navigator.mediaDevices", navigator.mediaDevices);
$("#alert").hide()
$("#record-contain").show();
navigator.mediaDevices.getUserMedia({audio: true, video: false}).then(function (stream) {
Expand Down
12 changes: 12 additions & 0 deletions docker-compose-selenium.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '2'

services:
selenium-tests:
build:
context: .
dockerfile: Dockerfile_selenium
shm_size: 2g
depends_on:
- web
networks:
- default
46 changes: 46 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
### Юнит тесты
#### Сборка и запуск проекта
Собираем и запускаем проект с конфигом `config.ini` или `testing.ini`
```bash
$ docker compose build

$ APP_CONF=../app_conf/testing.ini docker compose up
```

#### Запуск тестов
Запускаем тесты следующей командой
```bash
$ docker exec web_speech_trainer-web-1 bash -c 'cd /project/tests && pytest --ignore=selenium'
```

`--ignore=selenium` необходим (`pytest -s . -k 'not selenium'` не сработает), чтобы pytest не смотрел на selenium тесты, которые будут собраны в другом контейнере, иначе будут ошибки (selenium тесты будут ссылаться на отсутствующие в текущем контейнере зависимости)
### Selenium тесты

#### Сборка и запуск проекта
Сборка и запуск проекта с selenium тестами осуществляется c конфигом `testing.ini` следующим образом
```bash
$ docker compose -f docker-compose.yml -f docker-compose-selenium.yml build

$ APP_CONF=../app_conf/testing.ini docker compose -f docker-compose.yml -f docker-compose-selenium.yml up
```
#### Запуск тестов
Чтобы запустить selenium тесты необходимо выполнить следующую команду
```bash
$ docker exec web_speech_trainer-selenium-tests-1 bash -c 'pytest .'
```
От сюда же можно запустить и юнит тесты, описанным в разделе юнит тестов способом
#### Структура selenium тестов
##### Selenium session
Запускаемые тесты используют фикстуру `selenium_session` со `scope="module"`, объявленную в `conftest.py`. Она регистрируется в системе и возвращает экземпляр класса `SeleniumSession` с запущенным и настроенным драйвером. В нашем случае `scope="module"` значит, что для всех тестов в одном модуле будет использоваться один экземпляр `SeleniumSession` (не будет пересоздаваться от теста к тесту в одном модуле).

При необходимости можно объявлять другие фикстуры с другими настройками драйвера, например убрать симуляцию аудиопотока.
##### Training session
Класс `TrainingSession` является надстройкой над `SeleniumSession`. Можно сказать, что это API взаимодействия с любой тренировкой. Причем `SeleniumSession` может передаваться между такими надстройками, что позволяет создавать различные сценарии для тестирования.

Например, после завершения тренировки, использовать API для другой странички, чтобы полистать страничку со всеми тренировками и убедиться в наличии новой тренировки.
##### Test Main
`test_main.py` - входная точка тестирования, содержащая класс `TestMain`, который назовем сценарием тестирования. Данный класс может наследоваться от других сценариев тестирования, которые в данном контексте уже будут подсценариями. Например в `TestMain` используется подсценарий `SimpleTraining` проверяющий простую тренировку, но после тренировки можно использовать и другие подсценарии.

Исполнение тестов будет идти в порядке наследования, причем названия наследуемых классов не должны иметь префикс `Test`, иначе тесты могут дублироваться (вроде как принято использовать префикс `Base` для таких классов).

Наследование позволяет создавать различные сценарии тестирования. Каждый сценарий лучше описывать в отдельном файле вида `test_<название модуля с тестами>.py`, а их подсценарии либо в этом же файле, либо в файле без префикса `test_`, если планируется использование в других сценариях.
3 changes: 1 addition & 2 deletions tests/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
pytest==8.0.2
selenium==4.16.0
webdriver-manager==4.0.1
selenium==4.33.0
16 changes: 16 additions & 0 deletions tests/selenium/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import pytest

from selenium_session import SeleniumSession, ROOT_DIR, chrome_options
from app.config import Config

CONFIG_PATH = f'{ROOT_DIR}/app_conf/testing.ini'
AUDIO_FILE = f"{ROOT_DIR}/simple_phrases_russian.wav"

@pytest.fixture(scope="module")
def selenium_session():
Config.init_config(CONFIG_PATH)
session = SeleniumSession(Config.c, chrome_options(AUDIO_FILE))

yield session

session.end_session()
62 changes: 62 additions & 0 deletions tests/selenium/selenium_session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import requests
from time import sleep

from selenium.webdriver import Chrome

HOST = 'http://web:5000'
ROOT_DIR = '/usr/src/project'

from selenium.webdriver.chrome.options import Options

def chrome_options(audio_file=None):
chrome_options = Options()

chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--headless')
chrome_options.add_argument(f'--unsafely-treat-insecure-origin-as-secure={HOST}')

if audio_file is not None:
chrome_options.add_argument("--disable-user-media-security")
chrome_options.add_argument("--use-fake-device-for-media-stream")
chrome_options.add_argument("--use-fake-ui-for-media-stream")
chrome_options.add_argument(f'--use-file-for-fake-audio-capture={audio_file}')

return chrome_options

class SeleniumSession:
def __init__(self, config, chrome_options, requires_init=True):
self.__prepare_session(HOST, config, chrome_options, requires_init)

def __init_driver(self, chrome_options):
self.driver = Chrome(options=chrome_options)
self.session = requests.Session()

sleep(5)

def __registrate(self, config):
self.session.request('POST',f'{self.host}/lti', data={
'lis_person_name_full': config.testing.lis_person_name_full,
'ext_user_username': config.testing.session_id,
'custom_task_id': config.testing.custom_task_id,
'custom_task_description': config.testing.custom_task_description,
'custom_attempt_count': config.testing.custom_attempt_count,
'custom_required_points': config.testing.custom_required_points,
'custom_criteria_pack_id': config.testing.custom_criteria_pack_id,
'roles': config.testing.roles,
'lis_outcome_service_url': config.testing.lis_outcome_service_url,
'lis_result_sourcedid': config.testing.lis_result_source_did,
'oauth_consumer_key': config.testing.oauth_consumer_key,
})

def __prepare_session(self, host, config, chrome_options, requires_init):
self.host = host

self.__init_driver(chrome_options)

if requires_init:
self.driver.get(f'{self.host}/init/')

self.__registrate(config)

def end_session(self):
self.driver.quit()
27 changes: 27 additions & 0 deletions tests/selenium/simple_training.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from time import sleep

from selenium_session import ROOT_DIR
from training_session import Training

PRESENTATION_FILE = f"{ROOT_DIR}/test_data/test_presentation_file_0.pdf"
ESTIMATED_PROCESSING_TIME_IN_SECONDS = 100

class SimpleTraining:
def test_presentation_upload(self, selenium_session):
Training(selenium_session).upload_presentation(PRESENTATION_FILE)

def test_record_preparation(self, selenium_session):
Training(selenium_session).prepare_record()
sleep(5)

def test_button_next(self, selenium_session):
Training(selenium_session).next_slide()
sleep(5)

def test_training_session_end(self, selenium_session):
Training(selenium_session).end_training()
sleep(5)

def test_training_feedback(self, selenium_session):
got_feedback = Training(selenium_session).wait_for_feedback(ESTIMATED_PROCESSING_TIME_IN_SECONDS)
assert got_feedback, f"Проверка тренировки заняла более {ESTIMATED_PROCESSING_TIME_IN_SECONDS} секунд"
4 changes: 4 additions & 0 deletions tests/selenium/test_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from simple_training import SimpleTraining

class TestMain(SimpleTraining):
pass
70 changes: 0 additions & 70 deletions tests/selenium/test_training.py

This file was deleted.

15 changes: 0 additions & 15 deletions tests/selenium/testing.ini

This file was deleted.

Loading