Тестируем из нескольких браузеров одновременно в Selenium WebDriver

Подавляющее число автоматических тестов действует от имени единственного пользователя на протяжении всего тестового сценария. Однако бывают случаи (и я бы не сказал, что такое случаи единичны), требующие проверки взаимодействия нескольких различных пользователей через тестируемое приложение. В сегодняшней статье, мы разберем простой пример реализации такого поведения. Пример, как всегда, затронет чуть больше, чем того требуется для минимального освещения проблемы. Делается это для того, чтобы постепенно развивать в себе не только сугубо специфичные знания по Selenium API, но и тренировать навыки организации кода и в целом проектирования его архитектуры.

В конце я все таки дам минимальный пример очищенный от всей инфраструктурной обёртки. Только вебдрайвер, проверки и ничего лишнего.

Описание примера

Начнём как всегда с описания примера. В нашем пример мы откроем два параллельно работающих браузера. Оба браузера начнут работу с разными страницами и будут исполнять "условное" параллельные действия. Почему "условно"? Потому что фактически код не будет параллельным. Это бы потребовало добавления многопоточности, что увеличило бы объем строк, но не добавило бы принципиальной пользы примеру. Действия будут осуществляться последовательно, но в двух параллельно работающих браузерах. Возможно, в будущем я опишу пару примеров в которой реальная параллелизация действительно была бы необходимой.

Эти два параллельно запущенных браузера в процессе теста перейдут на другие страницы сайта (опять же каждый на свою) и для полноты картины (всё таки мы о тестировании говорим) проведут проверку заголовков своих страниц.

Для полного понимания реализации кода, представленного в примерах, полезно будет ознакомиться с понятием интерфейса, функционального интерфейса и лямбда-выражения.

Подготовка к разработке теста

Тест мы будем разрабатывать на языке Java. В моем примере я буду использовать браузер FireFox и операционную систему Linux. Также, кроме Selenium я планирую использовать Maven и JUnit. Если вы пока не знакомы с этими фреймворками - не беда. Концептуально они не влияют на тему статьи, а используются скорее для того чтобы минимизировать вспомогательный код, сконцентрировавшись на действительно важном. Для того, чтобы наш код получил доступ к необходимым библиотекам, убедитесь, что ваш файл pom.xml выглядит примерно так:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>webelement-click</groupId>
    <artifactId>selenium-svg-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.seleniumhq.selenium/selenium-java -->
        <dependency>
            <groupId>org.seleniumhq.selenium</groupId>
            <artifactId>selenium-java</artifactId>
            <version>3.141.59</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.5.2</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

</project>

После этого, скачайте, и разместите где-либо webdriver для вашей операционной системы. На момент написания статьи, официальным вебдрайвером для браузера Mozilla FireFox считается драйвер Geckodriver, доступный для скачивания по этой ссылке. Если вы используете Linux, не лишним также будет убедиться в том, что у вашего пользователя есть права на запуск исполняемого файла webdriver.

Добавляем немного архитектуры

Начиная разрабатывать проект тест-автоматизации с нуля, полезно бывает сразу оценить предстоящий объем и сложность работ (количество тестов, используемые библиотеки, вид и тип входных и выходных данных и т.д.) и заранее подумать над архитектурой, которая позволила бы эффективно добавлять новые и изменять старые тесты. Давайте добавим в наш проект сущность "шаг". Этот "шаг" мы опишем с помощью функционального интерфейса IStep.

package click.webelement.parallel;

import org.openqa.selenium.WebDriver;

public interface IStep {
    void doStep(WebDriver driver);
}

Этот интерфейс определяет только один метод doStep(), который в дальнейшем мы используем в сущности StepPerformer, которая (по задумке) должна отвечать за исполнения тестовых шагов. Посмотрим на реализацию класса StepPerformer.

package click.webelement.parallel;

import org.openqa.selenium.WebDriver;

public class StepPerformer {

    private WebDriver driver;

    public StepPerformer(WebDriver driver){
        this.driver = driver;
    }

    public void executeStep(IStep step){
        step.doStep(driver);
    }

    public void terminate(){
        if(driver != null){
            driver.quit();
        }
    }

}

Из кода мы видим, что, создавая объект класса StepPerformer ему будет необходимо передать объект класса WebDriver, который, в свою очередь, присваивается внутреннему одноименному полю класса. Кроме конструктора, в классе присутствует еще один метод. Этот метод и отвечает за исполнение тестового шага. Мы не знаем заранее какие шаги разработчики тестов захотят реализовать в будущем, но мы знаем, что они должны будут имплементировать интерфейс IStep, поэтому мы просто вызываем метод doStep() у передаваемого объекта и передаем в него тот driver, который мы зафиксировали при создании объекта. Таким образом, разработчиков шагов не будет волновать какой вебдрайвер использовать. Они будут использовать тот драйвер, который будет передан в их шаг через нашу архитектуру. Такая конструкция помогает добавлять и удалять функциональность, общую для исполнения всех шагов. Например, мы можем добавить логирование шага, либо замерить время его исполнения, добавив пару строк кода в в метод executeStep.

Кроме прочего, добавим в класс метод terminate() который будем использовать для "освобождения" вебдрайвера по окончанию нашего теста.

Создаём костяк теста

Напишем несколько строк кода для того чтобы обеспечить корректный запуск и корректную остановку наших тестов. В этом фрагменте мы, пользуясь аннотациями JUnit (если вы еще не знаете что такое аннотации, советую почитать про них), размечаем наш код таким образом чтобы фреймворк знал из каких компонент состоит наш тест и в каком порядке их надо запускать.

package click.webelement.parallel;

import org.junit.jupiter.api.*;
import org.openqa.selenium.By;
import org.openqa.selenium.firefox.FirefoxDriver;

public class TestParallelUsers {

    StepPerformer stepsOfOneUser;
    StepPerformer stepsOfAnotherUser;

    @BeforeAll
    public static void globalSetup(){
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Dev/webdrivers/geckodriver");
    }

    @BeforeEach
    public void setUp(){
        stepsOfOneUser = new StepPerformer(new FirefoxDriver());
        stepsOfAnotherUser = new StepPerformer(new FirefoxDriver());
        stepsOfOneUser.executeStep(d -> d.get("http://webelement.click/en/welcome"));
        stepsOfAnotherUser.executeStep(d -> d.get("http://webelement.click/en/selenium_and_svg_example_java"));
    }

    @AfterEach
    public void tearDown(){
        stepsOfOneUser.terminate();
        stepsOfAnotherUser.terminate();
    }

    @Test
    public void testTwoBrowsersSimultaneously(){
    }

    private void ezWait(){
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

Давайте взглянем на некоторые примечательные моменты, которые мы имеем в этом классе. Первый и основной - мы определяем два поля типа StepPerformer. По одному для каждого пользователя, участвующего в тесте. Напомню, что в нашей архитектуре эти сущности будут отвечать за исполнения тестовых шагов. Далее в методе globalSetup() мы указываем путь по которому располагается исполняемая программа нашего WebDriver (в вашем случае этот путь, скорее всего, будет отличаться). Перед началом каждого теста (конечно, пока что у нас всего один тест, но если придет время добавить второй - мы будем к этому готовы) мы открываем страницы, которые должны стать отправной точкой для каждого отдельного браузера.

Здесь мы впервые встречаемся с тем как реализуется "шаг" в нашей архитектуре. Для того чтобы ваша среда разработки корректно отображала такой синтаксис, необходимо включить в настройках поддержку синтаксиса Java 1.8. Форма передаваемого в метод параметра в данном примере называется лямбда-выражением. Можно обойтись и без него, но тогда придется добавить еще несколько строк кода, передавая анонимный объект в метод вместо лямбда-выражения. Итак, в методе setUp() перед каждым тестом мы выполняем для каждого пользователя "шаг", состоящий из обращения к первоначальному URL.

После каждого теста, мы должны обеспечить корректное закрытие наших драйверов, не забыв при этом проверить не указывают ли соответствующие поля "в никуда", чтобы избежать возможного NullPointerException.

В примере я покажу только один тест. В данной реализации он не имеет тела. Его мы добавим позже. А пока, в методе ezWait() мы опишем простейшую задержку, требуемую для демонстрационных целей. Выделяем её в отдельный метод мы для того, чтобы сократить количество и повысить читаемость тестового кода, т.к. Thread.sleep() может выбросить исключение, и мы должны его обработать.

Добавляем тест

После того, как мы подготовили всё необходимое с архитектурной точки зрения, добавление тестового кода остается делом техники. Итак, наш единственный тестовый метод будет выглядеть так:

@Test
public void testTwoBrowsersSimultaneously(){
    stepsOfOneUser.executeStep(d -> {
        d.findElement(By.xpath("//ul[@class='side-menu']/li[3]")).click();
        ezWait();
    });
    stepsOfAnotherUser.executeStep(d -> {
        d.findElement(By.xpath("//div[@id='menu-tags']/div[1]/a")).click();
        ezWait();
    });
    stepsOfOneUser.executeStep(d -> Assertions.assertTrue(d.getTitle().contains("Feedback")));
    stepsOfAnotherUser.executeStep(d -> Assertions.assertTrue(d.getTitle().contains("Search")));
}

Не смотря на то, что однострочные лямбда-выражения многократно повышают читаемость кода, язык допускает их многострочность, поэтому в тестовом примере мы не будем отклоняться от определенного в самом начале подхода. В каждый из шагов перехода на другую страницу мы добавили метод задержки, чтобы при запуске теста вы могли визуально оценить как проходит сценарий. Проверки в тесте мы также реализовали в виде "шагов".

Вообще, сложносоставные шаги - довольно частое явление. Предложенная в примере архитектура позволяет разработчикам реализовывать сложные шаги, которые можно будет затем переиспользовать в различных частях различных тестовых сценариев. Но подробнее об этом я напишу позже в отдельной статье.

Если убрать "архитектуру"

Если вы не хотите забивать голову лишними не относящимися напрямую к теме деталями, давайте посмотрим на то как бы выглядел минимально пригодный для запуска пример.

package click.webelement.parallel;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.firefox.FirefoxDriver;

public class TestParallelUsers {

    @Test
    public void minimalExample(){
        System.setProperty("webdriver.gecko.driver", "/home/alexey/Dev/webdrivers/geckodriver");
        WebDriver user1 = new FirefoxDriver();
        WebDriver user2 = new FirefoxDriver();
        user1.get("http://webelement.click/en/welcome");
        user2.get("http://webelement.click/en/selenium_and_svg_example_java");
        user1.findElement(By.xpath("//ul[@class='side-menu']/li[3]")).click();
        user2.findElement(By.xpath("//div[@id='menu-tags']/div[1]/a")).click();
        Assertions.assertTrue(user1.getTitle().contains("Feedback"));
        Assertions.assertTrue(user2.getTitle().contains("Search"));
        user1.quit();
        user2.quit();
    }

}

Остались вопросы? Задавайте их тут. Я постараюсь дополнить статью опираясь на ваши замечания.