Статьи

Запуск Selenium параллельно с любым инструментом модульного тестирования .NET


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

В то время как мои предпочтительные инструменты тестирования — NUnit и SpecFlow , метод, который я собираюсь предложить, должен работать с любым существующим тестовым набором, который вы, возможно, захотите использовать. Единственным предварительным условием является использование моделей страниц для переноса доступа к любой конкретной веб-странице.

В этой статье предполагается, что вы:

  • Уже умею писать тесты Selenium
  • Уже знаете, как использовать Selenium Grid
  • Уже знаете, как использовать шаблон Page Model
  • Уже знаете, как использовать выбранный вами тестовый ремень.

ХОРОШО. На главном событии.

Постановка задачи

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

Трудный способ сделать это — создать интерфейс, представляющий реальную модель страницы, а затем создать прокси-класс, который будет содержать список всех объектов реальной модели страницы, которые нам нужно вызвать. Каждый раз, когда вызывается свойство или метод прокси-сервера, все, что он должен сделать, это передать вызов в реальные объекты параллельно.

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

DynamicObject

Введите малоизвестный класс, DynamicObject . В .NET 4 Microsoft ввела динамическое ключевое слово. Одно из основных применений — для мест, где вам нужно иметь возможность объявить переменную в вашем коде, которую компилятор не будет знать, как разрешить тип до времени выполнения. Я мог бы использовать это несколько лет назад, когда у меня было две сборки, которые должны были ссылаться друг на друга. В этом случае я использовал отражение. Но динамический работал бы с гораздо меньшим количеством работы.

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

Мы также будем использовать библиотеку Task Parallel для реализации наших параллельных вызовов.

Для полноты и того, чтобы никто не запутался, пытаясь реализовать этот код, вам понадобятся следующие операторы using в верхней части файла CS.

Использование операторов и конструктора

using System;
using System.Collections.Concurrent;
using System.Dynamic;
using System.Reflection;
using System.Threading.Tasks;

Итак, начнем. Первое, что нам нужно, это объявление класса:

class ParallelPageModel<TPage>:  DynamicObject
{
}

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

Класс наследуется от DynamicObject, так что все наше совершенство на лету будет работать.

Далее нам понадобится место для хранения массива PageObjects, которые мы хотим прокси. Поэтому мы добавляем приватную переменную _page для этой цели.

private readonly TPage[] _pages;

Используя TPage [], мы создаем переменную того же типа, что и модели страниц, которые мы используем.

Далее нам нужен конструктор.

ParallelPageModel(params TPage[] pages)
{
    _pages = pages;
}

Используя ключевое слово params, мы можем передавать объекты страницы как массив или как отдельные параметры.

Волшебство происходит в трех переопределенных методах, которые находятся в DynamicObject:

  • TryInvokeMember — разрешает любые вызовы методов.
  • TrySetMember — разрешает любые установщики свойств
  • TryGetMember — разрешает любые свойства get

Итак, давайте добавим эти методы дальше:

public override bool TryInvokeMember
    (InvokeMemberBinder binder, 
      object[] args, 
      out object result)
{
}

public override bool TrySetMember
    (SetMemberBinder binder, object value)
{
}

public override bool TryGetMember
    (GetMemberBinder binder, out object result)
{
}

TryInvokeMember

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

Когда я впервые понял это, я начал с реализации цикла foreach, но мы собираемся перейти к использованию Parallel.ForEach ().

Parallel.ForEach () позволит нам передать массив и запустить лямбда-выражение для каждого элемента в массиве. Итак, наш цикл foreach будет выглядеть так:

var results = new ConcurrentBag<object>();
Parallel.ForEach(_pages, page =>
{
    var thisResult = typeof (TPage)
       .InvokeMember(binder.Name,
        BindingFlags.InvokeMethod | 
        BindingFlags.Public | 
        BindingFlags.Instance, 
        null, page, args);
    results.Add(thisResult);
});

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

Возвращаемый результат добавляется в нашу коллекцию ConcurrentBag. ConcurrentBag — это коллекция, специально созданная для параллельных вызовов. У нас могут возникнуть проблемы, если мы добавим что-то в коллекцию List <>, если не добавим некоторый контроль параллелизации вокруг него. Я за то, чтобы делать как можно меньше работы.

Второе, что мы хотим сделать, это обработать возвращаемые результаты.

Для этого нам нужно настроить базовый цикл foreach.

foreach (var thisResult in results)
{
}

Внутри цикла foreach мы обработаем сбор результатов.

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

if (thisResult is TPage)
{
    result = this;
}

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

else if (result != null)
{
    if (!result.Equals(thisResult)) // not the same value
    {
        throw new Exception
           ("Call to method returns different values.");
    }
}

Наконец, мы просто устанавливаем результат на то, что имеем на данный момент.

else
{
    result = thisResult;
}

И затем последнее, что мы хотим сделать, это вернуть true, чтобы сообщить системе, что мы смогли обработать метод.

TryGetMember

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

Фактически, единственное различие между этими двумя методами — это код внутри блока параметров Parallel.ForEach.

Итак, вот оно:

Parallel.ForEach(_pages, page => 
{
    var thisResult = typeof(TPage)
        .GetProperty(binder.Name).GetValue(page);
    results.Add(thisResult);
});

TrySetMember

TrySetMember — это самая легкая реализация из всех, так как не о чем беспокоиться.

Parallel.ForEach(_pages,
     page => typeof (TPage)
        .GetProperty(binder.Name).SetValue(page, value));
return true;

Таким образом, приведенный выше код будет работать, но вы не получите никакой помощи IntelliSense от Visual Studio, если будете использовать этот код без его настройки.

Нам нужен какой-то способ приведения объекта ParallelPageModel к типу TPage, который мы передаем.

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

Вам нужно будет добавить оператор использования.

using ImpromptuInterface;

И тогда вам нужно будет добавить этот метод в класс ParallelPageModel.

public TPage Cast()
{
    return this.ActLike();
}

Вы бы использовали это так:

IMyPageModel p = pageModelProxy.Cast();

Где IMyPageModel — это интерфейс, который определяет, как выглядит ваш настоящий класс PageModel.

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

Вызов ParallelPageModel

Чтобы настроить ParallelPageModel, ваш код должен выглядеть примерно так, если предположить, что у вас есть класс модели страницы MyPageModel с интерфейсом IMyPageModel.

var pages = new ConcurrentStack<IMyPageModel>();
Parallel.Invoke(
    () => 
        pages.Push(PageFactory.GetPageModel("FireFoxGrid"), 
    () => 
        pages.Push(PageFactory.GetPageModel("IE11Grid"));
var pagesArray = pages.ToArray();
MyTypedPage = 
    new ParallelPageModel<IMyPageModel>(pagesArray).Cast();

Соображения

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

Например, я предполагаю, что вы имеете дело только с простыми типами или типом модели страницы, для которой вы являетесь прокси. Здесь нет кода, который бы обрабатывал ситуацию, когда вызов метода вернул бы совершенно новую модель страницы. Поскольку код, который я тестирую, представляет собой набор одностраничных приложений, и я не тестирую навигацию на этом этапе, это не является для меня вопросом. Но это было бы относительно легко реализовать. Если бы я это сделал, я бы, вероятно, справился бы с этим, но создал бы подкласс этого основного класса, который выполняет основную часть работы и переопределяет метод Try * Member, который необходим для решения этой ситуации.Другой возможный способ справиться с ситуацией — передать список типов, которые необходимо обернуть в их собственный объект распараллеливания, в качестве параметров в конструкторе и добавить некоторый общий код в класс ParallelPageModel.

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

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

ParallelPageClass

Другие места, говорящие о параллельном селене

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