t.me/atinfo_chat Telegram группа по автоматизации тестирования

Тест падает без трейсов и не дебажится. OneTimeSetUp: No suitable constructor was found

csharp
visual-studio
webdriver
Теги: #<Tag:0x00007fb2f261e0e8> #<Tag:0x00007fb2f261dfa8> #<Tag:0x00007fb2f261de68>

(dim) #1

Привет, подскажите пожалуйста как побороть эту ошибку?
Трейс не выпадает. Дебаг не запускается.

namespace WebFormsAutomation
{
    [TestFixture(Category="BaseClassTests")]
    public abstract class BaseClass
    {
        protected static IWebDriver driver;

        public IWebDriver Driver
        {
            get { return driver; }
            set { Driver = driver; }
        }

        public BaseClass(IWebDriver driver)
        {
            Driver = driver;
        }

        [OneTimeSetUp()]
        public void TestInitialize()
        {
            driver = new ChromeDriver();
        }

        [OneTimeTearDown]
        public void TestCleanup()
        {
            driver.Quit();
        }
    }
}

----------<b>Common</b>
namespace WebFormsAutomation.PageObject
{
    public class Commons : BaseClass
    {
        public Commons(IWebDriver driver) : base(driver)
        {
        }

        public void OpenUrl(string url) => Driver.Navigate().GoToUrl(url);

        public bool FindElement(string element)
        {
            try
            {
                Driver.FindElement(By.CssSelector(element));
            }
            catch (NoSuchElementException)
            {
                return false;
            }
            return true;
        }

        public bool IsDisplayed(string element)
        {
            try
            {
                var searchedElement = Driver.FindElement(By.CssSelector(element));
                var result = searchedElement.Displayed;
            }
            catch (ElementNotVisibleException)
            {
                return false;
            }
            return true;
        }

-------<b>Scenario</b>

namespace WebFormsAutomation.PageObject
{
    public class AddRemoveElementScenarios : Commons
    {


        public AddRemoveElementScenarios(IWebDriver driver) : base(driver)
        {
        }

        public void AddElement(string url)
        {
            OpenUrl(url);
            FindElement(AddButton);
            IsDisplayed(AddButton);
        }   

        public readonly string AddButton = "div.example button";
    }
}

--------<b>Test</b>
namespace WebFormsAutomation.Tests
{
    [TestFixture]
    public class AddRemoveElementTest : AddRemoveElementScenarios
    {
        public AddRemoveElementTest(IWebDriver driver) : base(driver)
        {
        }

        [TestCase]
        public void AddElementTest()
        {
            OpenUrl("http://the-internet.herokuapp.com/add_remove_elements/");
            FindElement(AddButton);
            IsDisplayed(AddButton);
        }
    }
}

(Alexandr D.) #2

Фикстура не имеет конструктора по умолчанию, при этом она не принимает никаких параметров.
А вообще каша какая-то.


(dim) #3

Если не указываю методы и [TestFixture] оставляю пустой - та же проблема, а почему каша?


(Alexandr D.) #4
  1. У вас есть базовый класс для тестов - BaseClass
    Который не имеет конструктора по умолчанию из-за того, что в нём указан конструктор с параметром.
  2. Ваш Commons класс зачем-то наследуется от BaseClass, наследуя тем самым от него и атрибут TestFixture, и тоже не имеет конструктора по умолчанию.
  3. Ваш класс AddRemoveElementScenarios наследуется от Commons, наследуя заодно и всё от BaseClass, включая атрибут TestFixture, и тоже не имеет конструктора по умолчанию.

В общем полный треш какой-то :slight_smile:

Слишком много проблем, чтобы это ещё детальнее разбирать.

Так не делается.

Лично я бы не стал заморачиваться вообще с передачей драйвера во все классы что есть, а делал его через ThreadStatic.

И цепочка наследования должна выглядеть так (имхо):
Класс с тестами => Опционально дополнительный базовый класс, вносящий свою доп. логику присущую этому набору тестов, переопределяя [SetUp], [TearDown] и тд => Базовый класс всех тестов, отвечающий за жизненный цикл драйвера и прочих плюшек теста

А у вас за каким-то чудом и PO в этой цепочке находится и класс-хелпер, которые к тому же маркаются лишними атрибутами.


(Vladislav Abramov) #5

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

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


(Alexandr D.) #6

От чистой статики конечно лучше избавляться :slight_smile:

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

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

В спекфлоу если мне не изменяет память это делает встроенный в него механизм DI.
Если использовать DI, а не руками каждый раз в коде писать var page = new MainPage(driver), то конечно же это тоже удобно.

Тут кому как удобнее. Но если не использовать DI, то на мой взгляд ручное пропихивание драйвера это утомительно и рано или поздно приведет к ошибкам.


(Vladislav Abramov) #7

на работе как раз threadstatic юзаем, и приходится в методах инициализировать страницы каждый раз

дома попробовал DI, когда в конструкторе класса поднимаем браузер и в нем уже страницу, код почище получается, но надо проверять как параллелизация работает


(dim) #8

а можно пример пожалуйста, не уверен что
правильно понимаюю


(Vladislav Abramov) #9

у вас чистый nunit, я не могу привести пример. работаю со specflow, он автогенит nunit-овые классы


(Alexandr D.) #10

Эту проблему можно решить написав статичный вспомогательный метод, который сам будет отвечать за инициализацию страницы (если это надо, а если нет - брать уже инициализрованную, например).
Вся логика в одном месте, написал один раз и забыл.
Типа такого:

public static T GetPage<T>() where T : BasePage, new()
        {
        } 

Например в классе DriverManager есть такое поле:

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

Параметр true в конструкторе отвечает за то, что мы сможем “достучаться” до драйвера из любого потока. Это нужно в очень экзотических случаях.

Обращаться к драйверу соответственно через статик свойство:

public static IWebDriver Driver
        {
            get => DriverThreadLocal.Value;
         }

Инициализировать опять же тоже просто, например в методе с атрибутом [SetUp]:

DriverThreadLocal.Value = value;

В итоге в каждом потоке (параллельные тесты) будет свой драйвер.

А экзотический случай заключается в следующем: Например в начале теста надо что-то селектнуть в БД, но это занимает порядка 10-30 сек. Ждать бессмысленно, ведь в это время можно открыть браузер и дойти до шага, где эти данные понадобятся.
Поэтому запускается всё это добро в отдельных Task.Run( () => ).
Так вот каждый таск - это будет отдельный поток, в котором свой Driver отсутствует.

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

Лично я это делаю просто записывая хешкод созданного драйвера в момент его создания.

Выглядит это дело вот таким образом:

public static IWebDriver Driver
        {
            get
            {
                if (!AllureLifecycle.IsMainThread)
                {
                    var driverHashCode = DriversMainThreadHashCodes
                        .First(_ => _.testCaseId == TestContext.CurrentContext.Test.ID).driverHashCode;
                    var driver =
                        DriverThreadLocal.Values.FirstOrDefault(_ => _.GetHashCode() == driverHashCode);
                    if (driver == null)
                        throw new ArgumentNullException(
                            $"Не смогли найти драйвер основного потока теста. Основной драйвер имеет хэшкод {driverHashCode}, но в списке его нет: {DriverThreadLocal.Values}");
                    return driver;
                }

                if (!DriverThreadLocal.IsValueCreated)
                    throw new ArgumentNullException(
                        "Драйвер не проинициализирован. Прежде чем обращаться к драйверу, вызовите метод InitDriver");
                if (DriverThreadLocal.Value == null)
                    throw new ArgumentNullException(nameof(Driver), @"Driver is null! What the hell is going on?");
                return DriverThreadLocal.Value;
            }
            private set => DriverThreadLocal.Value = value;
        }