Тестирование UI с помощью Selenium в ASP.NET Core MVC

Внимание

Данный материал является частью цикла статей «Тестирование в ASP.NET Core». Не забудьте посмотреть другие статьи по этой теме :-)

  1. Модульное тестирование с помощью xUnit в ASP.NET Core MVC
  2. Тестирование контроллеров MVC в ASP.NET Core
  3. Интеграционное тестирование в ASP.NET Core MVC
  4. AntiForgeryToken для интеграционного тестирования в ASP.NET Core
  5. Тестирование UI с помощью Selenium в ASP.NET Core MVC

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

В этой статье мы собираемся использовать Selenium для написания автоматических тестов пользовательского интерфейса и на этом завершим нашу серию статей по тестированию. Selenium поддерживает множество различных браузеров, и в этой статье мы будем использовать ChromeDriver. Он также содержит много различных полезных методов (Navigate, GoToUrl, FindElement, SendKees, Click…), которые помогают нам управлять различными элементами HTML. Эти методы будут очень полезны в наших примерах.

Подготовка проекта и установка Selenium

Создайте новый тестовый проект xUnit (.NET Core) и нозовите его EmployeesApp.AutomatedUITests. После создания переименуйте существующий класс в AutomatedUITests:

Теперь мы можем открыть окно диспетчера пакетов NuGet и установить две необходимые библиотеки, Selenium.WebDriver и Selenium.Chrome.WebDriver:

Или вы можете использовать окно диспетчера пакетов:

PM> Установить пакет Selenium.WebDriver -Version 3.141.0

PM> Установить пакет Selenium.Chrome.WebDriver -Version 79.0.0

Это все, что нужно. Теперь мы готовы написать автоматизированные тесты пользовательского интерфейса.

Написание первого теста UI

Давайте откроем класс AutomatedUITests и изменим его, реализовав интерфейс IDisposable:

public class AutomatedUITests : IDisposable
{
    public void Dispose()
    {
    }
}

Мы собираемся использовать метод Dispose, чтобы закрыть окно chrome, открытое ChromeDriver, а также удалить его. Чтобы продолжить, давайте создадим наш драйвер:

public class AutomatedUITests : IDisposable
{
    private readonly IWebDriver _driver;
    public AutomatedUITests()
    {
        _driver = new ChromeDriver();
    }

    public void Dispose()
    {
        _driver.Quit();
        _driver.Dispose();
    }
}

Итак, мы создаем экземпляр объекта IWebDriver, используя класс ChromeDriver, а в методе Dispose удаляем его. После этого все готово для первого теста пользовательского интерфейса:

[Fact]
public void Create_WhenExecuted_ReturnsCreateView()
{
    _driver.Navigate()
        .GoToUrl("https://localhost:5001/Employees/Create");

    Assert.Equal("Create - EmployeesApp", _driver.Title);
    Assert.Contains("Please provide a new employee data", _driver.PageSource);
}

Мы используем метод Navigate, чтобы указать драйверу, что нужно переместить браузер в другое место, и с помощью метода GoToUrl мы указываем это местоположение. После перехода браузера к запрошенному URL-адресу будут заполнены свойства Title и PageSource объекта _driver.

Итак, мы просто делаем утверждения для этих свойств, чтобы убедиться, что мы действительно перешли на страницу Create. Прежде чем мы запустим окно обозревателя тестов, нам нужно запустить наше приложение без отладки (CTRL + F5), потому что для прохождения тестов пользовательского интерфейса требуется работающий сервер:

Как только мы запустим наш тест, мы увидим, что новое окно браузера открывается и вскоре после этого закрывается, потому что мы вызываем метод Quit в методе Dispose. Чуть позже наш тест пройдет.

Отлично!

Мы можем закрыть наше приложение и перейти к другим тестам.

Давайте напишем еще один тест, в котором мы проверяем, появляется ли сообщение об ошибке на экране, если мы заполняем некоторые поля ввода, а не все из них, и нажимает кнопку "Create":

[Fact]
public void Create_WrongModelData_ReturnsErrorMessage()
{
    _driver.Navigate()
        .GoToUrl("https://localhost:5001/Employees/Create");
    _driver.FindElement(By.Id("Name"))
        .SendKeys("Test Employee");

    _driver.FindElement(By.Id("Age"))
        .SendKeys("34");

    _driver.FindElement(By.Id("Create"))
        .Click();

    var errorMessage = _driver.FindElement(By.Id("AccountNumber-error")).Text;

    Assert.Equal("Account number is required", errorMessage);
}

Обратите внимание, что мы переходим к нужному месту с помощью методов Navigate и GoToUrl. После этого мы начинаем заполнять поля ввода. Конечно, сначала нужно найти элемент. Для этого мы используем выражение FindElement(By.Id("Name")).

Метод FindElement найдет элемент на HTML-странице и принимает параметр типа By. Класс By состоит из различных методов, которые позволяют нам искать различные элементы на нашей странице (Id, ClassName, CssSelector, TagName и т.д.).

Найдя элемент, мы используем метод SendKeys для его заполнения. Тот же процесс повторяется для элемента Age и кнопки Create, только для кнопки Create мы используем Click способ щелкнуть по нему.

Наконец, мы извлекаем сообщение об ошибке со страницы и делаем утверждение.

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

Давайте снова запустим наше приложение без отладки и запустим тест:

Мы видим, что это не удается, и сообщение довольно хорошо объясняет это. Метод FindElement не может найти нашу кнопку со значением Create для атрибута Id. Итак, мы можем проверить исходный код нашей страницы и найти допустимый атрибут или немного изменить наш код. Мы собираемся изменить код.

Давайте откроем представление Create в основном проекте и просто добавим атрибут Id к элементу кнопки:

<input type="submit" id="Create" value="Create" class="btn btn-primary" />

Теперь давайте запустим приложение и запустим окно обозревателя тестов:

Тест прошел.

Дополнительный тест пользовательского интерфейса

Давайте напишем еще один тест, в котором мы заполняем все поля, нажимаем кнопку "Create" и затем проверяем, загружена ли страница индекса с новым сотрудником:

[Fact]
public void Create_WhenSuccessfullyExecuted_ReturnsIndexViewWithNewEmployee()
{
    _driver.Navigate()
        .GoToUrl("https://localhost:5001/Employees/Create");
    _driver.FindElement(By.Id("Name"))
        .SendKeys("Another Test Employee ");

    _driver.FindElement(By.Id("Age"))
        .SendKeys("34");

    _driver.FindElement(By.Id("AccountNumber"))
        .SendKeys("123-9384613085-58");

    _driver.FindElement(By.Id("Create"))
        .Click();

    Assert.Equal("Index - EmployeesApp", _driver.Title);
    Assert.Contains("Another Test Employee ", _driver.PageSource);
    Assert.Contains("34", _driver.PageSource);
    Assert.Contains("123-9384613085-58", _driver.PageSource);
}

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

Отличная работа. Далее изменим наш код, чтобы исключить повторы и сделать его более читабельным.

Как сделать код еще лучше

Мы видим, что у нас много избыточного кода в наших методах тестирования, когда мы переходим к URI или находим различные элементы на HTML-странице.

Этого мы хотим избежать.

Этот шаблон, который мы собираемся использовать, называется шаблоном проектирования объектной модели страницы. Но мы не собираемся использовать класс PageFactory (как вы можете видеть во многих различных примерах), потому что он не поддерживается в .NET Core и становится устаревшим в .NET Framework.

Итак, начнем с создания нового класса EmployeePage в проекте EmployeesApp.UITests и его изменения:

 public class EmployeePage
{
    private readonly IWebDriver _driver;
    private const string URI = "https://localhost:5001/Employees/Create";
    private IWebElement NameElement => _driver.FindElement(By.Id("Name"));
    private IWebElement AgeElement => _driver.FindElement(By.Id("Age"));
    private IWebElement AccountNumberElement => _driver.FindElement(By.Id("AccountNumber"));
    private IWebElement CreateElement => _driver.FindElement(By.Id("Create"));

    public string Title => _driver.Title;
    public string Source => _driver.PageSource;
    public string AccountNumberErrorMessage => _driver.FindElement(By.Id("AccountNumber-error")).Text;

    public EmployeePage(IWebDriver driver)
    {
        _driver = driver;
    }

    public void Navigate() => _driver.Navigate()
        .GoToUrl(URI);

    public void PopulateName(string name) => NameElement.SendKeys(name);
    public void PopulateAge(string age) => AgeElement.SendKeys(age);
    public void PopulateAccountNumber(string accountNumber) => AccountNumberElement.SendKeys(accountNumber);
    public void ClickCreate() => CreateElement.Click();

}

Этот код довольно легко понять, потому что мы только извлекаем логику для поиска элементов HTML и добавляем некоторые методы для их заполнения и нажимаем кнопку Create. Мы дополнительно извлекаем логику для заголовка, источника и сообщения об ошибке.

После этих изменений мы можем изменить класс AutomatedUITests:

public class AutomatedUITests : IDisposable
{
    private readonly IWebDriver _driver;
    private readonly EmployeePage _page;
    public AutomatedUITests()
    {
        _driver = new ChromeDriver();
        _page = new EmployeePage(_driver);
        _page.Navigate();
    }
    [Fact]
    public void Create_WhenExecuted_ReturnsCreateView()
    {
        Assert.Equal("Create - EmployeesApp", _page.Title);
        Assert.Contains("Please provide a new employee data", _page.Source);
    }

    [Fact]
    public void Create_WrongModelData_ReturnsErrorMessage()
    {
        _page.PopulateName("New Name");
        _page.PopulateAge("34");
        _page.ClickCreate();

        Assert.Equal("Account number is required", _page.AccountNumberErrorMessage);
    }

    [Fact]
    public void Create_WhenSuccessfullyExecuted_ReturnsIndexViewWithNewEmployee()
    {
        _page.PopulateName("New Name");
        _page.PopulateAge("34");
        _page.PopulateAccountNumber("123-9384613085-58");
        _page.ClickCreate();

        Assert.Equal("Index - EmployeesApp", _page.Title);
        Assert.Contains("New Name", _page.Source);
        Assert.Contains("34", _page.Source);
        Assert.Contains("123-9384613085-58", _page.Source);
    }

    public void Dispose()
    {
        _driver.Quit();
        _driver.Dispose();
    }
}

Очевидно, что этот код намного чище и легче читается.

Вы можете запустить обозреватель тестов, чтобы убедиться, что все тесты проходят так же, как и раньше.