-
Notifications
You must be signed in to change notification settings - Fork 5
docs: parallelism #120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
docs: parallelism #120
Changes from all commits
578c847
fa7c1be
7440dd5
c98a6c5
4468c11
c0370dc
9e6ef78
f96dad9
d47f5ce
80ec6b9
7cb70b5
7817151
d1eaf7f
4695057
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| # Parallelism |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,352 @@ | ||
| # Параллельный запуск тестов | ||
|
|
||
| ## Параллельность в Testplane | ||
|
|
||
| Параллельность в Testplane — это возможность одновременно выполнять несколько тестовых сценариев в разных браузерных сессиях, чтобы сократить общее время прогона тестов. | ||
|
|
||
| ### sessionsPerBrowser | ||
|
|
||
| В конфигурационном файле объявляются типы браузеров в поле `browsers`. Для каждого типа браузера независимо настраивается, сколько экземпляров может работать одновременно: | ||
|
|
||
| ```typescript | ||
| // .testplane.config.ts | ||
| export default { | ||
| browsers: { | ||
| chrome: { | ||
| sessionsPerBrowser: 5, // до 5 параллельных сессий Chrome | ||
| }, | ||
| firefox: { | ||
| sessionsPerBrowser: 2, // до 2 параллельных сессий Firefox | ||
| }, | ||
| }, | ||
| }; | ||
| ``` | ||
shadowusr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| Параметр `sessionsPerBrowser` является ключевым в рамках управления параллельностью. Он определяет, сколько браузеров одного типа Testplane запустит одновременно. По умолчанию значение: `1`. | ||
|
|
||
| ### Воркеры | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Остальные разделы названы параметрами. Предлагаю сделать консистентно, чтобы и тут тогда было workers. |
||
|
|
||
| Параметр `workers` определяет, сколько дочерних процессов-воркеров будет запущено. Архитектурно это выглядит так: | ||
|
|
||
| - `Master`-процесс — координирует запуск, формирует очереди тестов и раздает задания воркерам | ||
| - `Worker`-процессы — непосредственно исполняют тесты | ||
|
|
||
| Каждый воркер — это отдельный поток, но внутри одного воркера тесты выполняются конкурентно: когда тест ждет ответа от браузера (`await`), воркер не простаивает, а переключается на следующий тест. Поэтому даже один воркер способен обслуживать множество параллельных браузерных сессий. | ||
|
|
||
| :::warning Важно | ||
| `workers` и `sessionsPerBrowser` — независимые ограничения. Увеличение числа воркеров не увеличивает реальную параллельность, если `sessionsPerBrowser` остается прежним. | ||
| ::: | ||
|
|
||
| ### testsPerSession | ||
|
|
||
| Параметр `testsPerSession` отвечает за то, сколько тестов можно запускать последовательно в одной сессии браузера. Он ограничивает переиспользование сессии, чтобы не допустить падения тестов из-за деградации браузера, и не имеет отношения к параллельному запуску тестов. | ||
|
|
||
| ### Как это работает | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Предлагаю назвать секцию "пример" |
||
|
|
||
| В рамках примера параметру `sessionsPerBrowser` присвоены значения 5: | ||
|
|
||
| ```typescript | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Форматирование в блоках кода съехало, т.к. не является валидным typescript кодом. Я ранее оставлял такой коммент, он актуален и тут: У нас в проекте работает prettier, который умеет форматировать код внутри таких сниппетов, поддерживая единый стиль отступов, кавычек, запятых на конце и т.д. Поэтому важно, чтобы если сниппет обозначен как const desiredCapabilities = {
"goog:chromeOptions": {
mobileEmulation: {
deviceName: "iPhone 12 Pro",
}
}
};либо как json, но главное, чтобы объявленный язык сниппета совпадал с реальностью. |
||
| chrome: { | ||
| headless: true, | ||
| desiredCapabilities: { | ||
| browserName: "chrome" | ||
| }, | ||
| sessionsPerBrowser: 5, // 5 параллельных сессий Chrome | ||
| waitTimeout: 10000, | ||
| } | ||
| ``` | ||
|
|
||
| и 2: | ||
|
|
||
| ```typescript | ||
| firefox: { | ||
| headless: true, | ||
| desiredCapabilities: { | ||
| browserName: "firefox" | ||
| }, | ||
| sessionsPerBrowser: 2, // 2 параллельные сессии Firefox | ||
| waitTimeout: 10000, | ||
| } | ||
| ``` | ||
|
|
||
| Значение параметра `workers`: | ||
|
|
||
| ```typescript | ||
| system: { | ||
| workers: 1, | ||
| }, | ||
| ``` | ||
|
|
||
| Далее тесты: | ||
|
|
||
| ```typescript | ||
| describe("test examples", () => { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Этот сниппет надо многократно сократить, можно полностью удалить тело тестов, оставив только названия, т.к. что происходит в самом тесте тут нам не важно |
||
| it("Поиск элемента по data-testid", async ({ browser }) => { | ||
| // Открываем страницу и ждем ее загрузки | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем элемент по атрибуту data-testid | ||
| const element = await browser.$('[data-testid="main-content"]'); | ||
|
|
||
| // Проверяем существование элемента в DOM | ||
| const isExisting = await element.isExisting(); | ||
| console.log("Элемент с data-testid существует:", isExisting); | ||
| }); | ||
|
|
||
| it("Поиск элемента на главной странице", async ({ browser }) => { | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем элемент по классу "navbar" | ||
| const navbar = await browser.$(".navbar"); | ||
|
|
||
| // Проверяем, отображается ли элемент на странице | ||
| const isDisplayed = await navbar.isDisplayed(); | ||
| console.log("Навбар отображается:", isDisplayed); | ||
| }); | ||
|
|
||
| it("Поиск элемента по id на главной странице", async ({ browser }) => { | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем элемент по id "__docusaurus" | ||
| const main = await browser.$("#__docusaurus"); | ||
|
|
||
| // Проверяем, отображается ли элемент на странице | ||
| const isDisplayed = await main.isDisplayed(); | ||
| console.log("Элемент отображается:", isDisplayed); | ||
| }); | ||
|
|
||
| it("Поиск элемента по типу атрибута", async ({ browser }) => { | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем кнопку по атрибуту type="button" | ||
| // Формат селектора: element[type="value"] | ||
| const button = await browser.$('button[type="button"]'); | ||
|
|
||
| // Проверяем существование элемента в DOM | ||
| const isExisting = await button.isExisting(); | ||
| console.log("Кнопка существует:", isExisting); | ||
| }); | ||
|
|
||
| it("Поиск элемента по тексту", async ({ browser }) => { | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем элемент по тексту внутри него | ||
| const link = await browser.$('//a[text()="Docs"]'); | ||
|
|
||
| // Проверяем существование элемента в DOM | ||
| const isExisting = await link.isExisting(); | ||
| console.log("Элемент с текстом существует:", isExisting); | ||
| }); | ||
|
|
||
| it("Поиск элемента по атрибуту", async ({ browser }) => { | ||
| await browser.openAndWait("https://testplane.io/"); | ||
|
|
||
| // Ищем элемент по атрибуту type | ||
| const button = await browser.$('//button[@type="button"]'); | ||
|
|
||
| // Проверяем существование элемента в DOM | ||
| const isExisting = await button.isExisting(); | ||
| console.log("Элемент с атрибутом существует:", isExisting); | ||
| }); | ||
|
|
||
| it("Поиск кнопки с помощью метода getByRole", async ({ browser }) => { | ||
| await browser.url("https://testplane.io/"); | ||
|
|
||
| const button = await browser.getByRole("button", { name: "Get started" }); | ||
|
|
||
| await button.click(); | ||
| }); | ||
| }); | ||
| ``` | ||
|
|
||
| #### Как работает параллелизм в данном примере | ||
|
|
||
| Для Chrome открывается до 5 окон браузера одновременно. Каждое из них — это отдельная независимая сессия со своим `sessionId`: | ||
|
|
||
| ```bash | ||
| Тест 1 → сессия chrome:abc123 → открывается окно Chrome #1 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. В этой секции в целом пример классный, но давай объединим секции chrome+firefox и засинкаем названия с примером выше, чтобы получилось примерно так, имея конфиг: Получим такую последовательность: |
||
| Тест 2 → сессия chrome:def456 → открывается окно Chrome #2 | ||
| Тест 3 → сессия chrome:ghi789 → открывается окно Chrome #3 | ||
| Тест 4 → сессия chrome:jkl012 → открывается окно Chrome #4 | ||
| Тест 5 → сессия chrome:mno345 → открывается окно Chrome #5 | ||
| ``` | ||
|
|
||
| Для Firefox логика та же, но одновременно работают только 2 окна. Это ограничение задано через `sessionsPerBrowser`: | ||
|
|
||
| ```bash | ||
| Тест 1 → сессия firefox:fb59f7 → открывается окно Firefox #1 | ||
| Тест 2 → сессия firefox:660ee0 → открывается окно Firefox #2 | ||
| Тест 3 → ждёт в очереди... | ||
| как только тест 1 завершился → сессия fb59f7 освободилась | ||
| Тест 3 → сессия firefox:fb59f7 → то же окно Firefox #1, новый тест | ||
| ``` | ||
|
|
||
| :::warning Важно | ||
| `workers` управляет количеством `Node.js`-процессов, а `sessionsPerBrowser` — количеством одновременных браузерных сессий внутри каждого воркера. При `workers`: `1` все 7 сессий управляются одним процессом. | ||
| ::: | ||
|
|
||
| ## Шардирование | ||
|
|
||
| При наличии тысяч тестов время одного запуска может быть неприемлемо большим даже при максимальном параллелизме. В таких случаях используют шардирование — разбивку всего набора тестов на несколько независимых частей (чанков), которые запускаются параллельно на разных машинах или в разных `CI`-джобах. | ||
|
|
||
| ### Плагин testplane-chunks | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Несоответствие — здесь testplane-chunks, далее @testplane/chunks. Правильно использовать везде @testplane/chunks |
||
|
|
||
| Плагин `@testplane/chunks` позволяет распараллелить запуск тестов на нескольких серверах, тем самым ускорив процесс. Однако сам плагин не занимается какой-либо оркестрацией, распараллеливанием запуска или слиянием получившихся отдельных отчетов в один итоговый отчет. | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. А чем же занимается? =) |
||
|
|
||
| #### Установка и подключение | ||
|
|
||
| Для установки выполните команду: | ||
|
|
||
| ```bash | ||
| npm install -D @testplane/chunks | ||
| ``` | ||
|
|
||
| И укажите параметры в файле `testplane.config.ts`. | ||
|
|
||
| ```typescript | ||
| module.exports = { | ||
| plugins: { | ||
| "@testplane/chunks": { | ||
| count: 7, // Разбить тесты на 7 порций (чанков) | ||
| run: 1, // Запустить первую порцию | ||
| }, | ||
|
|
||
| // другие плагины Testplane... | ||
| }, | ||
|
|
||
| // другие настройки Testplane... | ||
| }; | ||
| ``` | ||
|
|
||
| #### Расшифровка параметров конфигурации | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Думаю можно убрать секцию |
||
|
|
||
| | Параметр | Тип | По умолчанию | Описание | | ||
| | -------- | -------- | ------------ | ----------------------------------------------------------------- | | ||
| | `count` | `Number` | `1` | Количество порций (чанков), на которые нужно разбить набор тестов | | ||
| | `run` | `Number` | `1` | Номер чанка, тесты из которого нужно запустить | | ||
|
|
||
| ### Как разбить запуск тестов | ||
|
|
||
| В качестве примера будет использован следующий набор тестов: | ||
|
|
||
| ```text | ||
| project/ | ||
| ├── .testplane.conf.js | ||
| ├── package.json | ||
| └── tests/ | ||
| ├── registration.testplane.js # 500 тестов | ||
| ├── payment.testplane.js # 500 тестов | ||
| └── auth.testplane.js # 500 тестов | ||
| ``` | ||
|
|
||
| ```javascript | ||
| //registration.testplane.js | ||
|
|
||
| describe("Регистрация", () => { | ||
| it("тест 1 - пустой email показывает ошибку", async ({ browser }) => {}); | ||
| it("тест 2 - невалидный формат email", async ({ browser }) => {}); | ||
| it("тест 3 - пустой пароль показывает ошибку", async ({ browser }) => {}); | ||
|
|
||
| // ... | ||
|
|
||
| // тест 500 | ||
| }); | ||
| ``` | ||
|
|
||
| ```javascript | ||
| //payment.testplane.js | ||
|
|
||
| describe("Оплата", () => { | ||
| it("тест 1 - пустой номер карты показывает ошибку", async ({ browser }) => {}); | ||
| it("тест 2 - невалидный номер карты", async ({ browser }) => {}); | ||
| it("тест 3 - истёкший срок действия карты", async ({ browser }) => {}); | ||
|
|
||
| // ... | ||
|
|
||
| // тест 500 | ||
| }); | ||
| ``` | ||
|
|
||
| ```javascript | ||
| //auth.testplane.js | ||
|
|
||
| describe("Авторизация", () => { | ||
| it("тест 1 - пустой email показывает ошибку", async ({ browser }) => {}); | ||
| it("тест 2 - пустой пароль показывает ошибку", async ({ browser }) => {}); | ||
| it("тест 3 - неверный пароль показывает ошибку", async ({ browser }) => {}); | ||
|
|
||
| // ... | ||
|
|
||
| // тест 500 | ||
| }); | ||
| ``` | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Тут надо рассказать или сослаться на то, как объединить отчеты этих чанков в 1 с помощью команды merge-reports html-reporter. |
||
| Резделение тестов на чанки: | ||
|
|
||
| ```css | ||
| Чанк 0 → тесты 1, 2, ..., 500 (≈ первая треть) | ||
| Чанк 1 → тесты 1, 2, ..., 500 (≈ вторая треть) | ||
| Чанк 2 → тесты 1, 2, ..., 500 (≈ третья треть) | ||
| ``` | ||
|
|
||
| Запуск каждого чанка отдельно через терминал: | ||
|
|
||
| ```bash | ||
| # Терминал 1 — Чанк 0 | ||
| CHUNKS_COUNT=3 CHUNKS_CURRENT=0 npx testplane | ||
|
|
||
| # Терминал 2 — Чанк 1 | ||
| CHUNKS_COUNT=3 CHUNKS_CURRENT=1 npx testplane | ||
|
|
||
| # Терминал 2 — Чанк 2 | ||
| CHUNKS_COUNT=3 CHUNKS_CURRENT=2 npx testplane | ||
| ``` | ||
|
|
||
| ### Как плагин делит тесты | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Думаю это можно убрать. |
||
|
|
||
| После сортировки по `fullTitle` набор тестов делится на 3 чанка: | ||
|
|
||
| ```css | ||
| ┌──────────┬──────────┬──────────┐ | ||
| │ Чанк 0 │ Чанк 1 │ Чанк 2 │ | ||
| ├──────────┼──────────┼──────────┤ | ||
| │ тест 1 │ тест 1 │ тест 1 │ | ||
| │ тест 2 │ тест 2 │ тест 2 │ | ||
| │ ... │ ... │ ... │ | ||
| │ тест 500 │ тест 500 │ тест 500 │ | ||
| └──────────┴──────────┴──────────┘ | ||
| ``` | ||
|
|
||
| :::tip Примечание | ||
| Главная идея чанков: если `workers` распределяет тесты внутри одного процесса, то чанки делят тесты между несколькими независимыми процессами — например, на разных `CI`-агентах или на одной машине через `concurrently`. | ||
| ::: | ||
|
|
||
| ### Как объединить отчеты | ||
|
|
||
| Чтобы объединить несколько отчетов в один, используйте команду `merge-reports`. Она принимает пути к директориям с отчетами, файлам баз данных или к файлам `databaseUrls.json`, после чего создает новый html-отчет в папке назначения с данными из всех переданных отчетов. | ||
|
|
||
| Пример использования: | ||
|
|
||
| ```bash | ||
| npx html-reporter merge-reports report-dir/ path-to-database.db path-to-databaseUrls.json -d dest-report -h foo=bar | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Давай упростим пример: |
||
| ``` | ||
|
|
||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Давай здесь все таки добавим секцию "пример github actions". В ней кратко скажем: Ниже показан пример запуска @testplane/chunks в GitHub Actions. Тесты разбиваются на 3 чанка, каждый чанк выполняется в отдельном matrix job, а затем отчёты всех чанков скачиваются из S3 и объединяются в один итоговый отчёт. И под кат занесем полный пример: Codename: Testplane
on:
pull_request:
permissions:
id-token: write
contents: read
jobs:
chunks:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
chunk: [1, 2, 3]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx testplane --chunks-count 3 --chunks-run ${{ matrix.chunk }}
- uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-central-1
- uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --follow-symlinks
env:
AWS_S3_BUCKET: ${{ secrets.S3_BUCKET }}
AWS_REGION: eu-central-1
SOURCE_DIR: testplane-report
DEST_DIR: testplane/${{ github.run_id }}/chunk-${{ matrix.chunk }}
merge-report:
runs-on: ubuntu-latest
needs: chunks
if: ${{ always() }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- uses: aws-actions/configure-aws-credentials@v5
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: eu-central-1
- run: |
mkdir -p reports/chunk-1 reports/chunk-2 reports/chunk-3
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-1/ reports/chunk-1 --recursive
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-2/ reports/chunk-2 --recursive
aws s3 cp s3://${{ secrets.S3_BUCKET }}/testplane/${{ github.run_id }}/chunk-3/ reports/chunk-3 --recursive
- run: |
npx testplane merge-reports \
reports/chunk-1 \
reports/chunk-2 \
reports/chunk-3 \
--output merged-report
- uses: jakejarvis/s3-sync-action@v0.5.1
with:
args: --follow-symlinks
env:
AWS_S3_BUCKET: ${{ secrets.S3_BUCKET }}
AWS_REGION: eu-central-1
SOURCE_DIR: merged-report
DEST_DIR: testplane/${{ github.run_id }}/merged |
||
| ## Рекомендуемые настройки и их расчет | ||
|
|
||
| #### Воркеры | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Здесь тоже workers |
||
|
|
||
| Рекомендуемое значение — 8. Любое другое не должно превышать количество ядер CPU. | ||
|
|
||
| #### sessionsPerBrowser | ||
|
|
||
| При локальном запуске значение не превышает 5. В ином случает примерное значение можно вычислить путем деления доступных ресурсов на требуемое для конкретного браузера. При расчете помните о затратах на запуск самих тестов. | ||
|
|
||
| #### Количество чанков | ||
|
|
||
| При расчете количества чанков необходимо отталкиваться от: | ||
|
|
||
| - количества доступных браузеров | ||
| - оверхеда на создание чанка | ||
| - желаемого общего времени прогона | ||
|
|
||
| Рекомендация — не меньше 500 тестов на чанк. | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Предлагаю здесь написать немного по-другому, т.к. это не возможность, которой можно пользоваться, а можно нет, а неотъемлемая часть продукта:
Testplane запускает тесты параллельно. Для этого запускается сразу несколько браузеров одновременно, которые управляются с помощью одного или нескольких воркеров. Параллельное выполнение тестов позволяет значительно сократить общее время прогона тестов.