C# + NUnit. ThreadLocal Webdriver. "No active session with ID" exception

Всем привет.
Пытаюсь запустить тесты параллельно с использованием ThreadLocal.
Подскажите, в ThreadLocal нужно ложить драйвер или экземпляр класса,в котором есть драйвер?
Как правильно делать driver.Quit() в таком случае? Натыкаюсь на “No active session with ID” exception.
И вообще, есть ли другие способы распаралелить UI тесты без использования ThreadLocal?

Через ThreadLocal вполне нормальный вариант.

Вот простой пример.

Делаем поле.

private static readonly ThreadLocal<IWebDriver> DriverThreadLocal = new ThreadLocal<IWebDriver>(true);

Где-нибудь пишем строку создания драйвера (либо сделать сеттер в свойстве):

DriverThreadLocal.Value = new ChromeDriver(cds, chromeOptions, TimeSpan.FromSeconds(ParametersCmd.DriverCmdTimeout));

Для удобного обращения к драйверу в тестах делаем свойство

public static IWebDriver Driver
{
    get
    {       
        if (!DriverThreadLocal.IsValueCreated)
            throw new ArgumentNullException(
                "Драйвер не проинициализирован.");
        return DriverThreadLocal.Value;
    }
}

А убивать такой драйвер можно либо через свойство, либо напрямую.

Например через свойство выше:

Driver?.Quit();
Driver?.Dispose();
1 лайк

Cделал все так, как описано выше.
Тесты запускаются параллельно, но есть нюанс с Driver.Quit().
Не получается закрыть сессию. И самое интересное то, что эту штука воспроизводится только при Run режиме. В Debug режиме все хорошо.

Т.е при вызове метода Driver.Quit(), драйвер уже null по понятно почему.

Код можно посмотреть тут: https://github.com/Roman1137/Tests_For_TestInfrastructure_Course/tree/experiment

Скачал ваш проект, выбрал DirectConnection, запустил все тесты - всё прошло без ошибок.

Как воспроизвести?

В общем, посмотрел внимательнее.

Проблема в том, что вы вызываете килл браузера в [OneTimeTearDown].

Браузер надо уничтожать после каждого теста, в [TearDown].

Так же как и создавать браузер надо для теста в [Setup], а не в [OneTimeSetUp]

В вашей же реализации, поток один раз создаёт Application, и в дальнейшем уже не вызывает для каждого теста метод инициализации драйвера, ибо он вшит в конструктор.

Спасибо большое!
Дело в том, что я хочу распаралелить тесты по фикстурам: чтобы на один потом запускалась одна сессия и в этой сессии ранились тесты а одного класса.
Насколько это имеет смысл? Или лучше создавать драйвер для каждого теста, т.е для каждого теста будет отдельная сессия и отдельно будет подниматься браузер?
Бесспорно, это изолирует тесты друг от друга, но требует больше времени на выполнение (запуск и закрытие браузера)

Одна сессия - один тест.
Это избавит вас от лишних проблем.
Самая банальная из которых - сессия внезапно сдохнет и все ваши недозапущенные тесты так и не запустятся.
Эта зависимость тестов друг от друга которая не несет никакого смысла.
К тому же, при использовании Selenoid, время подъема браузера в контейнере настолько маленькое, что этим можно пренебречь.

2 лайка

Значит при использовании NUnit нужно использовать Parallelizable(ParallelScope.All) вместо Parallelizable(ParallelScope.Fixtures)?

Хотя, нет. У меня получилось запустить тесты с ParallelScope.Fixtures.
Думаю, что разница между ParallelScope.All и ParallelScope.Fixtures состоит в том, что с использованием ParallelScope.Fixtures - мы будем иметь ровно столько потоков, сколько фикстур у нас есть. В каждом потоке будут запускаться по очереди тесты из фикстур. Если мы используем ParallelScope.All - то будет выделяться поток на каждый тест.
Ограничить количество поток можно с в обеих случаях помощью [assembly:LevelOfParallelism(3)].

Думаю, что если запускать для каждого теста свой браузер, то ParallelScope.Fixtures не имеет смысла. Лучше использовать ParallelScope.All - так все потоки будут равномерно нагружены(например может быть ситуция, когда в одной фикстуре 1 тест, а в другой 50 и если использовать ParallelScope.Fixtures - то поток с 1 тестом будет простаивать)

1 лайк

Итого, тесты получилось запустить параллельно двумя способами:

  1. С использованием ThreadLocal.

     private static ThreadLocal<IWebDriver> DriverThreadlocal = new ThreadLocal<IWebDriver>();
    
     public static IWebDriver Driver
     {
         get
         {
             if (!DriverThreadlocal.IsValueCreated)
             {
                 throw new ArgumentException("Driver is not initialized!");
             }
    
             return DriverThreadlocal.Value;
         }
     }
    

2)А так же, мне посоветовали использовать потокобезопасный словарь.

    private static ConcurrentDictionary<string, IWebDriver> DriverCollection = new ConcurrentDictionary<string, IWebDriver>();

    public static IWebDriver Driver
    {
        get
        {
            return DriverCollection.First(pair => pair.Key == TestContext.CurrentContext.Test.ID).Value;
        }

        set => DriverCollection.TryAdd(TestContext.CurrentContext.Test.ID, value);
    }

Работающие тесты можно найти по ссылкам

  1. Вариант со Threadlocal - GitHub - Roman1137/Tests_For_TestInfrastructure_Course at parallel_experiment_threadlocal,
  2. Вариант с ConcurrentDictionary -
    GitHub - Roman1137/Tests_For_TestInfrastructure_Course