Почему статики запретили, а потом разрешили?

Почему статики запретили, а потом разрешили?

Selenide Advent Calendar
День 9
09.12.19

Привет!

9 день рождественского календаря Selenide.
Расскажу о том, что сильнее всего волнует общественность.

Почему статики запретили в 5.0.0, а потом снова разрешили?

Короткий ответ: запретили случайно. Но правильно сделали. 

А теперь подробнее. 

Коротко о хранении вебдрайверов в селениде

Селенид изначально хранил вебдрайверы в ThreadLocal.
Это позволяет запускать тесты параллельно: в разных потоках - разные вебдрайверы. Код выглядит примерно так:

class WebDriverRunner {
  private static final ThreadLocal<WebDriver> webdriver = new ThreadLocal<>();
  private static final ThreadLocal<SelenideProxyServer> proxy = new ThreadLocal<>();
}

Скажем, метод $("a").click() работает примерно так:

  WebDriverRunner.webdriver.get().findElement("a").click();

Коротко про SelenideDriver

Но ThreadLocal накладывает ограничение: ты не можешь использовать в одном потоке два вебдрайвера (а также в двух потоках - один и тот же вебдрайвер, но до этого мы пока не дошли).

Это мы и собирались решить в Selenide 5.0.0. Мы сделали специальный класс SelenideDriver, позволяющий использовать два вебдрайвера в одном тесте:

SelenideDriver browser1 = new SelenideDriver();
SelenideDriver browser2 = new SelenideDriver();

browser1.open("https://google.com");
browser2.open("http://yandex.ru");

browser1.$(h1).shouldHave(text("Google"));
browser2.$(h1).shouldHave(text("Yandeks"));

Для этого пришлось сделать большой рефакторинг, так чтобы внутри самого селенида нигде не использовались старые добрые статические методы open(), $ и $$. Везде, где нужен вебдрайвер, селенид теперь использовал параметр SelenideDriver. Да-да, пришлось его прокидывать во все методы как параметр.

И вот тут главный вопрос

А что делать со старыми добрыми статическими методами open(), $ и $$? Они должны откуда-то взять инстанс SelenideDriver и передать его дальше в кишки селенида. Откуда его взять?

Selenide 5.0.0: Статикам наносят удар

На тот момент этот вопрос решили так (как оказалось, это решение сильно повлияло на ход дел).
В класс WebDriverRunner добавили третий ThreadLocal:

  private static final ThreadLocal<SelenideDriver> selenideDriver = new ThreadLocal<>();

Сам SelenideDriver - это, грубо говоря, очень простой класс с двумя полями WebDriver и SelenideProxyServer.
И в целом всё хорошо работало и решало поставленную задачу.

Чего я не мог предусмотреть на тот момент - это того, что многие люди определяли веб-элементы в статических полях:

public class MyPageObject {
  public static SelenideElement fname = $("#fname");
  public static SelenideElement lname = $("#lname");
}

Да ещё и переоткрывали браузер между тестами.

Важный нюанс:

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

Конечно, это происходило из-за плохого кода тестов, но мы же создали селенид, чтобы помогать людям, верно?
Поэтому с версии 5.0.0 селенид стал чётко говорить: “Браузера нет, работать не могу. Вызови сначала метод open(), тогда поговорим.”

И что же свалилось?

Совпадение этих двух факторов привело к следующей проблеме:

  1. Человек создаёт статическую переменную static SelenideElement fname = $("#fname").
  2. Этот fname запоминает, с каким инстансом SelenideDriver он был создан.
  3. Человек в конце теста закрывает браузер
  4. В следующем тесте человек открывает новый браузер и снова обращается к статическому полю fname.
  5. А fname обращается к своему SelenideDriver, с которым он был изначально создан.
  6. И кидает ошибку, потому что тот браузер уже закрылся.

Больше года - с сентября 2018 до октября 2019 - я пытался объяснить всем, что статические переменные - это зло, не надо их использовать.
Сделал даже специально доклад “Антистатик”.

Но в итоге сдался из тех соображений, что слишком много людей, которым пришлось бы переписывать свои тесты. Даже если они согласны насчёт злостности статических переменных, переписать всё - это зачастую неподъёмная задача.
Мы же создали селенид, чтобы помогать людям, верно?

Selenide 5.4.0: Статики победили

Как в итоге решили эту проблему?

Довольно просто. Мы заменили ThreadLocal<SelenideDriver> на статическую переменную (да, вы не ослышались :))

private static final SelenideDriver = new ThreadLocalSelenideDriver();

Теперь наш “статический” SelenideDriver - один на всех. Он никогда не закрывается. Все статические поля, созданные с ним, будут жить вечно. Но он и не хранит в себе поля WebDriver и SelenideProxyServer. Он каждый берёт их из тредлокалов WebDriverRunner.

P.S.

Поэтому в версии 5.4.0 пропал метод WebDriverRunner.getSelenideDriver().

Я очень удивился, что многие люди уже успели его использовать. Люди, я вас не понимаю! Как вы умудряетесь всё так неправильно использовать?
Ок, я не подумав, сделал его публичным. Моя ошибка. Но он не упомянут ни в каких пресс-релизах, ни в какой документации. Как могла кому-то прийти в голову идея его использовать? Как эта магия вообще происходит?

Что теперь?

Мы снова вернули возможность объявлять SelenideElement в статических полях.
Но пожалуйста, не злоупотребляйте этим.
Я всё ещё это не одобряю. :)

Андрей Солнцев

ru.selenide.org

09.12.19