TL;DR

  • WebdriverIO расширяет WebDriver автоматическими ожиданиями, $() селекторами и встроенным test runner
  • Multiremote управляет несколькими браузерами одновременно — тестируй чаты, совместное редактирование, real-time фичи
  • Миграция с Selenium: замени findElement(By.css()) на $(), убери явные ожидания, используй конфиг вместо программной настройки

Подходит для: JavaScript/TypeScript команд, которым нужен современный DX с совместимостью WebDriver Пропусти если: Нужна поддержка нескольких языков или команда уже продуктивна с Selenium

WebdriverIO эволюционировал из простой обёртки над WebDriver в комплексный фреймворк для end-to-end тестирования. Его плагиновая архитектура, возможности multiremote и мощные функции расширяемости делают его привлекательным выбором для современной автоматизации тестирования.

Я мигрировал Selenium-сьют из 300 тестов на WebdriverIO — кодовая база сократилась на 40%, время выполнения упало с 25 до 12 минут (параллельный запуск через maxInstances), а процент flaky тестов снизился с 8% до менее 2% благодаря автоматическим ожиданиям.

Это руководство покрывает три аспекта, которые важнее всего для команд, внедряющих WebdriverIO:

  1. Расширяемость — кастомные команды, сервисы и репортеры
  2. Multiremote — синхронизированные тесты в нескольких браузерных сессиях
  3. Миграция — переход с Selenium WebDriver на WebdriverIO

Обзор Архитектуры WebdriverIO

Основные Компоненты

Архитектура WebdriverIO состоит из нескольких взаимосвязанных слоев:

Слой Протокола

  • webdriver - Реализация протокола W3C WebDriver
  • devtools - Поддержка протокола Chrome DevTools
  • appium - Протокол мобильной автоматизации

Основной Слой

  • @wdio/cli - Интерфейс командной строки и test runner
  • @wdio/config - Парсер конфигурации
  • @wdio/utils - Общие утилиты

Слой Интеграции

  • Services - Интеграции с внешними инструментами (Selenium, Appium, Sauce Labs)
  • Reporters - Форматировщики результатов тестов (Allure, Spec, JUnit)
  • Frameworks - Адаптеры фреймворков тестирования (Mocha, Jasmine, Cucumber)

Этот модульный дизайн позволяет выборочное принятие функций и простую расширяемость.

Расширяемость: Создание Пользовательских Решений

Пользовательские Команды

WebdriverIO позволяет расширять как объект browser, так и отдельные элементы пользовательскими командами. Эта возможность критически важна для создания предметно-ориентированных DSL тестирования и уменьшения дублирования кода.

Пользовательские Команды на Уровне Browser

// wdio.conf.js
export const config = {
    before: function() {
        // Добавить пользовательскую команду к объекту browser
        browser.addCommand('loginAs', async function(username, password) {
            await browser.url('/login');
            await $('#username').setValue(username);
            await $('#password').setValue(password);
            await $('button[type="submit"]').click();
            await browser.waitUntil(
                async () => (await browser.getUrl()).includes('/dashboard'),
                {
                    timeout: 5000,
                    timeoutMsg: 'Логин не перенаправил на dashboard'
                }
            );
        });

        // Асинхронная команда с логикой повторных попыток
        browser.addCommand('waitForApiReady', async function(endpoint, maxRetries = 5) {
            let attempts = 0;
            while (attempts < maxRetries) {
                try {
                    const response = await browser.executeAsync((endpoint, done) => {
                        fetch(endpoint)
                            .then(res => done({ status: res.status, ok: res.ok }))
                            .catch(err => done({ error: err.message }));
                    }, endpoint);

                    if (response.ok) {
                        return true;
                    }
                } catch (error) {
                    console.log(`Попытка ${attempts + 1} проверки API провалилась`);
                }
                attempts++;
                await browser.pause(1000);
            }
            throw new Error(`API ${endpoint} не готов после ${maxRetries} попыток`);
        });
    }
};

Пользовательские Команды на Уровне Элемента

browser.addCommand('clickIfDisplayed', async function() {
    // 'this' ссылается на элемент
    if (await this.isDisplayed()) {
        await this.click();
        return true;
    }
    return false;
}, true); // true указывает на команду уровня элемента

browser.addCommand('setValueAndVerify', async function(value) {
    await this.setValue(value);
    const actualValue = await this.getValue();
    if (actualValue !== value) {
        throw new Error(`Несоответствие значения: ожидалось "${value}", получено "${actualValue}"`);
    }
}, true);

// Использование в тестах
await $('#dismissModal').clickIfDisplayed();
await $('#email').setValueAndVerify('test@example.com');

Переопределение Команд

Вы можете переопределять существующие команды для изменения поведения по умолчанию:

// Добавить автоматический скриншот при ошибках клика
const originalClick = browser.click;
browser.addCommand('click', async function(...args) {
    try {
        return await originalClick.apply(this, args);
    } catch (error) {
        const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
        await browser.saveScreenshot(`./screenshots/click-failure-${timestamp}.png`);
        throw error;
    }
}, true);

Пользовательские Сервисы

Сервисы расширяют возможности WebdriverIO, подключаясь к жизненному циклу тестов. Они идеальны для настройки тестовой инфраструктуры, управления внешними зависимостями или реализации пользовательских отчетов.

Структура Сервиса

// services/DatabaseService.js
import { MongoClient } from 'mongodb';

export default class DatabaseService {
    constructor(options) {
        this.options = options;
        this.client = null;
        this.db = null;
    }

    // Вызывается один раз перед всеми тестами
    async onPrepare(config, capabilities) {
        console.log('Подключение к базе данных...');
        this.client = await MongoClient.connect(this.options.connectionString);
        this.db = this.client.db(this.options.dbName);
    }

    // Вызывается перед каждым набором тестов
    async before(capabilities, specs) {
        // Сделать базу данных доступной для тестов
        global.testDb = this.db;

        // Заполнить тестовыми данными
        if (this.options.seedData) {
            await this.seedDatabase();
        }
    }

    // Вызывается после каждого теста
    async afterTest(test, context, { passed }) {
        if (!passed && this.options.captureStateOnFailure) {
            const state = await this.db.collection('users').find({}).toArray();
            test.dbState = state;
        }
    }

    // Вызывается после каждого набора тестов
    async after(result, capabilities, specs) {
        // Очистить тестовые данные
        if (this.options.cleanupAfterSuite) {
            await this.cleanupDatabase();
        }
    }

    // Вызывается один раз после всех тестов
    async onComplete(exitCode, config, capabilities) {
        console.log('Закрытие соединения с базой данных...');
        await this.client.close();
    }

    async seedDatabase() {
        await this.db.collection('users').insertMany([
            { username: 'testuser1', email: 'test1@example.com', role: 'user' },
            { username: 'admin1', email: 'admin@example.com', role: 'admin' }
        ]);
    }

    async cleanupDatabase() {
        await this.db.collection('users').deleteMany({ email: /@example\.com$/ });
    }
}

Конфигурация Сервиса

// wdio.conf.js
import DatabaseService from './services/DatabaseService.js';

export const config = {
    services: [
        ['chromedriver'],
        [DatabaseService, {
            connectionString: 'mongodb://localhost:27017',
            dbName: 'test_db',
            seedData: true,
            cleanupAfterSuite: true,
            captureStateOnFailure: true
        }]
    ]
};

Реальный Пример Сервиса: Mock API Сервер

// services/MockApiService.js
import express from 'express';

export default class MockApiService {
    constructor(options = {}) {
        this.port = options.port || 3001;
        this.app = express();
        this.server = null;
        this.mocks = new Map();
    }

    async onPrepare() {
        this.app.use(express.json());

        // Динамический mock endpoint
        this.app.all('*', (req, res) => {
            const key = `${req.method}:${req.path}`;
            const mock = this.mocks.get(key);

            if (mock) {
                res.status(mock.status || 200).json(mock.response);
            } else {
                res.status(404).json({ error: 'Mock не найден' });
            }
        });

        return new Promise((resolve) => {
            this.server = this.app.listen(this.port, () => {
                console.log(`Mock API сервер запущен на порту ${this.port}`);
                resolve();
            });
        });
    }

    before() {
        // Добавить helper к объекту browser
        browser.addCommand('mockApi', (method, path, response, status = 200) => {
            const key = `${method.toUpperCase()}:${path}`;
            this.mocks.set(key, { response, status });
        });

        browser.addCommand('clearMocks', () => {
            this.mocks.clear();
        });
    }

    async onComplete() {
        if (this.server) {
            await new Promise((resolve) => this.server.close(resolve));
            console.log('Mock API сервер остановлен');
        }
    }
}

Пользовательские Репортеры

Репортеры форматируют и выводят результаты тестов. Пользовательские репортеры обеспечивают интеграцию с собственными дашбордами, системами уведомлений или специализированными CI/CD пайплайнами.

// reporters/SlackReporter.js
import WDIOReporter from '@wdio/reporter';
import axios from 'axios';

export default class SlackReporter extends WDIOReporter {
    constructor(options) {
        super(options);
        this.webhookUrl = options.webhookUrl;
        this.failures = [];
    }

    onTestFail(test) {
        this.failures.push({
            title: test.title,
            parent: test.parent,
            error: test.error.message,
            stack: test.error.stack
        });
    }

    async onRunnerEnd(runner) {
        if (this.failures.length === 0) return;

        const message = {
            text: `⚠️ Обнаружены Упавшие Тесты (${this.failures.length})`,
            blocks: [
                {
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: `*${this.failures.length} тест(ов) провалились*`
                    }
                },
                {
                    type: 'divider'
                },
                ...this.failures.slice(0, 5).map(failure => ({
                    type: 'section',
                    text: {
                        type: 'mrkdwn',
                        text: `*${failure.parent} > ${failure.title}*\n\`\`\`${failure.error}\`\`\``
                    }
                }))
            ]
        };

        try {
            await axios.post(this.webhookUrl, message);
        } catch (error) {
            console.error('Не удалось отправить уведомление в Slack:', error.message);
        }
    }
}

Multiremote: Синхронизированное Тестирование в Нескольких Браузерах

Multiremote — это уникальная возможность WebdriverIO одновременно управлять несколькими сеансами браузера. Это бесценно для тестирования:

  • Функций совместной работы в реальном времени (чат, видеозвонки, совместное редактирование)
  • Межбраузерной коммуникации
  • Многопользовательских рабочих процессов
  • Адаптивного дизайна на разных устройствах

Базовая Настройка Multiremote

// wdio.conf.js
export const config = {
    capabilities: {
        browser1: {
            capabilities: {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    args: ['--window-size=1920,1080']
                }
            }
        },
        browser2: {
            capabilities: {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    args: ['--window-size=1920,1080']
                }
            }
        }
    }
};

Примеры Multiremote Тестов

Тестирование Чата в Реальном Времени

describe('Чат в реальном времени', () => {
    it('должен синхронизировать сообщения между пользователями', async () => {
        // Пользователь 1 входит в систему
        await browser1.loginAs('user1@test.com', 'password123');
        await browser1.url('/chat/general');

        // Пользователь 2 входит в систему
        await browser2.loginAs('user2@test.com', 'password123');
        await browser2.url('/chat/general');

        // Пользователь 1 отправляет сообщение
        const messageText = `Тестовое сообщение ${Date.now()}`;
        await browser1.$('#messageInput').setValue(messageText);
        await browser1.$('#sendButton').click();

        // Проверить, что Пользователь 2 получает сообщение
        await browser2.waitUntil(
            async () => {
                const messages = await browser2.$$('.chat-message');
                const texts = await Promise.all(
                    messages.map(msg => msg.getText())
                );
                return texts.some(text => text.includes(messageText));
            },
            {
                timeout: 5000,
                timeoutMsg: 'Пользователь 2 не получил сообщение'
            }
        );

        // Пользователь 2 видит правильного отправителя
        const lastMessage = await browser2.$('.chat-message:last-child');
        const sender = await lastMessage.$('.sender-name').getText();
        expect(sender).toBe('user1');
    });

    it('должен показывать индикаторы ввода', async () => {
        // Пользователь 1 начинает печатать
        await browser1.$('#messageInput').click();
        await browser1.$('#messageInput').keys('П');

        // Пользователь 2 должен видеть индикатор ввода
        await browser2.waitForDisplayed('.typing-indicator', { timeout: 2000 });
        const indicatorText = await browser2.$('.typing-indicator').getText();
        expect(indicatorText).toContain('user1 печатает');

        // Пользователь 1 прекращает печатать
        await browser1.$('#messageInput').clearValue();
        await browser1.pause(3000); // Ждать таймаут ввода

        // Индикатор должен исчезнуть
        await browser2.waitForDisplayed('.typing-indicator', {
            timeout: 2000,
            reverse: true
        });
    });
});

Совместное Редактирование Документов

describe('Совместное редактирование документов', () => {
    const documentId = 'test-doc-123';

    before(async () => {
        // Настройка: Оба пользователя переходят к одному документу
        await Promise.all([
            browser1.loginAs('editor1@test.com', 'pass123'),
            browser2.loginAs('editor2@test.com', 'pass123')
        ]);

        await browser1.url(`/documents/${documentId}`);
        await browser2.url(`/documents/${documentId}`);

        // Ждать загрузки документа
        await Promise.all([
            browser1.waitForDisplayed('#editor', { timeout: 5000 }),
            browser2.waitForDisplayed('#editor', { timeout: 5000 })
        ]);
    });

    it('должен показывать одновременные правки в реальном времени', async () => {
        // Редактор 1 печатает в параграфе 1
        await browser1.$('#editor p:nth-child(1)').click();
        await browser1.keys(['End']);
        const text1 = ' Добавлено редактором 1.';
        await browser1.keys(text1.split(''));

        // Проверить, что Редактор 2 видит изменение
        await browser2.waitUntil(
            async () => {
                const content = await browser2.$('#editor').getText();
                return content.includes(text1);
            },
            { timeout: 3000, timeoutMsg: 'Редактор 2 не увидел изменения Редактора 1' }
        );

        // Редактор 2 печатает в параграфе 2 одновременно
        await browser2.$('#editor p:nth-child(2)').click();
        await browser2.keys(['End']);
        const text2 = ' Добавлено редактором 2.';
        await browser2.keys(text2.split(''));

        // Проверить, что Редактор 1 видит изменение Редактора 2
        await browser1.waitUntil(
            async () => {
                const content = await browser1.$('#editor').getText();
                return content.includes(text2);
            },
            { timeout: 3000, timeoutMsg: 'Редактор 1 не увидел изменения Редактора 2' }
        );

        // Проверить, что оба редактора видят полный документ
        const finalContent1 = await browser1.$('#editor').getText();
        const finalContent2 = await browser2.$('#editor').getText();
        expect(finalContent1).toBe(finalContent2);
        expect(finalContent1).toContain(text1);
        expect(finalContent1).toContain(text2);
    });

    it('должен обрабатывать разрешение конфликтов', async () => {
        // Симулировать прерывание сети для browser1
        await browser1.throttle('offline');

        // Browser1 делает изменения в оффлайне
        await browser1.$('#editor p:nth-child(1)').click();
        await browser1.keys(['Command', 'a']); // Выделить все
        await browser1.keys('Офлайн изменения редактором 1');

        // Browser2 делает другие изменения, пока browser1 в оффлайне
        await browser2.$('#editor p:nth-child(1)').click();
        await browser2.keys(['Command', 'a']);
        await browser2.keys('Онлайн изменения редактором 2');

        // Восстановить соединение browser1
        await browser1.throttle('online');

        // Ждать разрешения конфликта
        await browser.pause(2000);

        // Проверить, что конфликт был обработан (зависит от реализации)
        const conflictDialog1 = await browser1.$('.conflict-dialog');
        const conflictDialog2 = await browser2.$('.conflict-dialog');

        expect(await conflictDialog1.isDisplayed() || await conflictDialog2.isDisplayed()).toBe(true);
    });
});

Продвинутые Паттерны Multiremote

Кросс-Девайсное Адаптивное Тестирование

export const config = {
    capabilities: {
        desktop: {
            capabilities: {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    args: ['--window-size=1920,1080']
                }
            }
        },
        tablet: {
            capabilities: {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    mobileEmulation: {
                        deviceName: 'iPad'
                    }
                }
            }
        },
        mobile: {
            capabilities: {
                browserName: 'chrome',
                'goog:chromeOptions': {
                    mobileEmulation: {
                        deviceName: 'iPhone 12 Pro'
                    }
                }
            }
        }
    }
};

describe('Адаптивный макет', () => {
    it('должен отображать соответствующую навигацию для каждого устройства', async () => {
        await Promise.all([
            desktop.url('/'),
            tablet.url('/'),
            mobile.url('/')
        ]);

        // Desktop должен показывать полную навигацию
        const desktopNav = await desktop.$('nav.desktop-nav');
        expect(await desktopNav.isDisplayed()).toBe(true);

        // Tablet может показывать сжатую навигацию
        const tabletNav = await tablet.$('nav.tablet-nav');
        expect(await tabletNav.isDisplayed()).toBe(true);

        // Mobile должен показывать меню-гамбургер
        const mobileHamburger = await mobile.$('.hamburger-menu');
        expect(await mobileHamburger.isDisplayed()).toBe(true);

        const mobileFullNav = await mobile.$('nav.desktop-nav');
        expect(await mobileFullNav.isDisplayed()).toBe(false);
    });
});

Руководство по Миграции: С Selenium WebDriver на WebdriverIO

Миграция с Selenium WebDriver на WebdriverIO требует понимания как концептуальных различий, так и изменений API.

Ключевые Концептуальные Различия

АспектSelenium WebDriverWebdriverIO
Обработка ПромисовТребуются явные промисы/async-awaitАвтоматическая синхронизация (в sync режиме)
Поиск ЭлементовМногословный (driver.findElement(By.css(...)))Краткий ($('selector'))
ОжиданияНеобходимы ручные явные ожиданияАвтоматическое умное ожидание
КонфигурацияТребуется программная настройкаОснован на конфигурационном файле
Test RunnerТребуется отдельный фреймворк (Jest, Mocha)Встроенный test runner
Логика ПовторовРучная реализацияВстроенные повторы элементов

Сопоставление Миграции API

Выбор Элементов

// Selenium WebDriver
const { By } = require('selenium-webdriver');
const element = await driver.findElement(By.css('#login-button'));
const elements = await driver.findElements(By.css('.list-item'));

// WebdriverIO
const element = await $('#login-button');
const elements = await $$('.list-item');

Взаимодействие с Элементами

// Selenium
const input = await driver.findElement(By.id('email'));
await input.clear();
await input.sendKeys('test@example.com');
await driver.findElement(By.id('submit')).click();

// WebdriverIO
await $('#email').clearValue();
await $('#email').setValue('test@example.com');
await $('#submit').click();

Навигация

// Selenium
await driver.get('https://example.com/login');
await driver.navigate().back();
await driver.navigate().forward();
await driver.navigate().refresh();

// WebdriverIO
await browser.url('/login'); // Относительно baseUrl
await browser.back();
await browser.forward();
await browser.refresh();

Ожидания и Ожидаемые Условия

// Selenium
const { until } = require('selenium-webdriver');
await driver.wait(until.elementLocated(By.id('result')), 5000);
await driver.wait(until.elementIsVisible(element), 5000);

// WebdriverIO (автоматическое ожидание)
await $('#result').waitForDisplayed({ timeout: 5000 });
await $('#result').waitForEnabled({ timeout: 5000 });
await browser.waitUntil(
    async () => (await $('#counter').getText()) === '10',
    { timeout: 5000, timeoutMsg: 'Счетчик не достиг 10' }
);

Page Objects

// Page Object Selenium
class LoginPage {
    constructor(driver) {
        this.driver = driver;
    }

    async open() {
        await this.driver.get('https://example.com/login');
    }

    async login(username, password) {
        await this.driver.findElement(By.id('username')).sendKeys(username);
        await this.driver.findElement(By.id('password')).sendKeys(password);
        await this.driver.findElement(By.css('button[type="submit"]')).click();
    }

    async getErrorMessage() {
        const element = await this.driver.findElement(By.css('.error-message'));
        return await element.getText();
    }
}

// Page Object WebdriverIO
class LoginPage {
    get usernameInput() { return $('#username'); }
    get passwordInput() { return $('#password'); }
    get submitButton() { return $('button[type="submit"]'); }
    get errorMessage() { return $('.error-message'); }

    async open() {
        await browser.url('/login');
    }

    async login(username, password) {
        await this.usernameInput.setValue(username);
        await this.passwordInput.setValue(password);
        await this.submitButton.click();
    }

    async getErrorMessage() {
        return await this.errorMessage.getText();
    }
}

Пошаговый Процесс Миграции

Шаг 1: Установить WebdriverIO

npm install --save-dev @wdio/cli
npx wdio config

Шаг 2: Создать Конфигурационный Файл

// wdio.conf.js
export const config = {
    specs: ['./test/specs/**/*.js'],
    maxInstances: 5,
    capabilities: [{
        browserName: 'chrome',
        'goog:chromeOptions': {
            args: ['--disable-gpu', '--no-sandbox']
        }
    }],
    logLevel: 'info',
    baseUrl: 'http://localhost:3000',
    waitforTimeout: 10000,
    framework: 'mocha',
    mochaOpts: {
        timeout: 60000
    },
    reporters: ['spec']
};

Шаг 3: Мигрировать Структуру Тестов

// До: Selenium с Mocha
const { Builder } = require('selenium-webdriver');

describe('Тесты Логина', function() {
    let driver;

    before(async function() {
        driver = await new Builder().forBrowser('chrome').build();
    });

    after(async function() {
        await driver.quit();
    });

    it('должен успешно войти', async function() {
        await driver.get('https://example.com/login');
        // ... логика теста
    });
});

// После: WebdriverIO
describe('Тесты Логина', () => {
    it('должен успешно войти', async () => {
        await browser.url('/login');
        // ... логика теста - объект browser автоматически доступен
    });
});

Шаг 4: Обработать Асинхронные Паттерны

// Selenium: Явное разрешение промисов
const elements = await driver.findElements(By.css('.item'));
const texts = await Promise.all(elements.map(el => el.getText()));

// WebdriverIO: Упрощенная асинхронная обработка
const texts = await $$('.item').map(el => el.getText());

Шаг 5: Обновить Утверждения

// Selenium с assert
const assert = require('assert');
const title = await driver.getTitle();
assert.strictEqual(title, 'Ожидаемый Заголовок');

// WebdriverIO с expect (встроенный)
await expect(browser).toHaveTitle('Ожидаемый Заголовок');
await expect($('#result')).toHaveText('Успех');

Чеклист Миграции

  • Установить WebdriverIO и настроить wdio.conf.js
  • Конвертировать инициализацию драйвера в конфигурационный файл
  • Обновить селекторы элементов на синтаксис WebdriverIO ($, $$)
  • Заменить явные ожидания автоматическим ожиданием WebdriverIO
  • Конвертировать page objects для использования геттеров
  • Обновить утверждения на expect матчеры WebdriverIO
  • Настроить репортеры (Allure, Spec и т.д.)
  • Настроить интеграцию CI/CD с WebdriverIO
  • Мигрировать пользовательские утилиты и хелперы
  • Обновить документацию и материалы онбординга

AI-ассистенты в разработке WebdriverIO

AI-инструменты ускоряют разработку тестов на WebdriverIO при стратегическом использовании.

Что AI делает хорошо:

  • Генерирует кастомные команды по описанию на естественном языке
  • Конвертирует Selenium тесты в синтаксис WebdriverIO (автоматически обрабатывает ~80%)
  • Создаёт boilerplate page objects с правильными getters
  • Предлагает стратегии селекторов для сложных DOM-структур

Что требует человека:

  • Логика синхронизации multiremote (тайминги зависят от контекста)
  • Решение, какие ожидания оставить явными vs неявными
  • Решения по дизайну тестов — какие сценарии требуют multiremote
  • Отладка flaky тестов (AI не имеет контекста сессии)

Полезные промпты:

“Конвертируй этот Selenium Java тест в WebdriverIO TypeScript. Используй $() селекторы, убери явные ожидания, добавь expect assertions.”

“Создай WebdriverIO кастомную команду для drag-and-drop, которая работает с shadow DOM элементами.”

“Сгенерируй multiremote setup для тестирования видеозвонков с 3 участниками.”

Совет по миграции: Отдавай AI свои Selenium page objects по одному. Проси конвертировать и объяснить каждое изменение. Проверяй предположения об автоматических ожиданиях — иногда нужны явные ожидания для анимаций или медленных API.

FAQ

WebdriverIO лучше чем Selenium?

WebdriverIO построен на протоколе WebDriver, но добавляет значительные улучшения developer experience: автоматические ожидания устраняют большинство проблем синхронизации, синтаксис $() более лаконичен чем findElement(By.css()), а встроенный test runner берёт на себя конфигурацию, которую Selenium требует настраивать отдельно. Для JavaScript/TypeScript команд WebdriverIO обычно более продуктивен. Selenium остаётся лучшим выбором для команд, которым нужна поддержка нескольких языков программирования или у которых уже есть глубокие инвестиции в инфраструктуру Selenium.

Что такое WebdriverIO multiremote?

Multiremote позволяет управлять несколькими инстансами браузера одновременно из одного теста. Вместо запуска отдельных тестовых процессов, ты определяешь именованные браузерные инстансы (например, userA и userB) и оркестрируешь их действия вместе. Это необходимо для тестирования real-time функций коллаборации, чат-приложений, мультиплеерных игр или любых сценариев, где несколько пользователей взаимодействуют с одной системой одновременно.

Сложно ли мигрировать с Selenium на WebdriverIO?

Усилия средние — большинство команд завершают миграцию за 1-2 недели для test suite среднего размера. Основные изменения: заменить findElement(By.css()) на $(), убрать явные ожидания (WebdriverIO обрабатывает ожидания автоматически), конвертировать page objects на JavaScript getters вместо методов, перенести конфигурацию драйвера из кода в wdio.conf.js. Самая большая сложность обычно — адаптироваться к поведению автоматических ожиданий и определить тесты, которым всё ещё нужны явные ожидания.

Можно ли использовать WebdriverIO с Cucumber?

Да. У WebdriverIO есть официальный сервис @wdio/cucumber-framework, который интегрируется бесшовно. Ты пишешь Gherkin feature файлы как обычно, определяешь step definitions используя API WebdriverIO, и запускаешь всё через WDIO test runner. Интеграция поддерживает Cucumber теги, хуки, таблицы данных и все стандартные возможности Cucumber. Многие команды мигрируют с Protractor+Cucumber на WebdriverIO+Cucumber с минимальными изменениями в своих feature файлах.

Может ли WebdriverIO тестировать мобильные приложения?

Да. У WebdriverIO есть первоклассная интеграция с Appium через @wdio/appium-service. Ты используешь те же $() селекторы, кастомные команды и page object паттерны для мобильного тестирования. Это значит, что веб и мобильные test suites используют один фреймворк, утилиты и знания команды. WebdriverIO поддерживает Android и iOS нативные, гибридные и мобильные веб-приложения через Appium.

Подходит ли WebdriverIO для CI/CD?

Отлично подходит. Подход с конфигурационным файлом делает настройку CI простой — не нужна программная инициализация драйвера. Встроенное параллельное выполнение (maxInstances), поддержка headless-браузеров и стандартные репортеры (Allure, JUnit XML, Spec) интегрируются с любой CI-системой. Большинство команд запускают WDIO в GitHub Actions, Jenkins или GitLab CI с минимальной настройкой.

Официальные ресурсы

Смотрите также