Автоматизированное тестирование в разных окружениях
Вот у меня есть тест. Очень простой:
1. Открываю Яндекс.
2. Пишу «отус».
3. Проверяю, что первый результат — это ссылка на сайт.
public class test1 { private WebDriver driver; @Before public void setUp() { WebDriverManager.chromedriver().setup(); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(10, TimeUnit.SECONDS); } @Test public void test() { driver.get("https://yandex.ru"); driver.findElement(cssSelector("#text")).sendKeys("otus", Keys.ENTER); String text = driver .findElement(cssSelector("[aria-label='Результаты поиска']")) .findElement(cssSelector("li h2")) .getText(); assertEquals(text, "OTUS - Онлайн-образование – Профессиональные курсы"); } @After public void tearDown() { if (driver != null) { driver.quit(); } } }
Но я хочу выполнять тестирование на странице yandex.ru и на странице ya.ru.
Пример, конечно, такой себе, но если представить, что вместо yandex.ru и ya.ru у нас, скажем, какие-нибудь dev.myapp.com и preprod.myapp.com, то это вполне жизненная ситуация, когда мы хотим тестировать разные окружения.
А от окружений часто зависят и другие параметры: например, логин-пароль, подключения к БД, url для обращения по REST API и всё такое.
Но чтобы пример было легко воспроизвести, позволю себе такое вот допущение.
Итак, чтобы запустить тест на ya.ru, мне нужно внести изменения в код теста —
поменять параметр у метода
@Test public void test() { driver.get("https://ya.ru"); driver.findElement(cssSelector("#text")).sendKeys("otus", Keys.ENTER); String text = driver .findElement(cssSelector("[aria-label='Результаты поиска']")) .findElement(cssSelector("li h2")) .getText(); assertEquals(text, "OTUS - Онлайн-образование – Профессиональные курсы"); }
Это сработает. Теперь я могу тестировать и yandex, и ya (dev и preprod).
Идея с редактированием кода проваливается, как только мы переносим тесты на CI: там редактировать код для каждого запуска не получится. Да и параметров может быть с десяток – в одном из них я обязательно ошибусь (я себя знаю), и весь прогон тестов будет завален.
Параметры
Любой, кто в Java дольше 1,5 часов, знает, что можно задавать параметры.
public class test2 { private WebDriver driver; private String url; private long timeout; @Before public void setUp() { WebDriverManager.chromedriver().setup(); url = System.getProperty("test.url", "https://yandex.ru"); timeout = Long.valueOf(System.getProperty("test.timeout", "10")); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(timeout, TimeUnit.SECONDS); } @Test public void test() { driver.get(url); driver.findElement(cssSelector("#text")).sendKeys("otus", Keys.ENTER); String text = driver .findElement(cssSelector("[aria-label='Результаты поиска']")) .findElement(cssSelector("li h2")) .getText(); assertEquals(text, "OTUS - Онлайн-образование – Профессиональные курсы"); } @After public void tearDown() { if (driver != null) { driver.quit(); } } }
Теперь есть параметры: url и timeout. Их я могу задавать при запуске, добавив в команду запуска
Чуточку лучше. Код написан один раз и я могу смело запускать его на CI-сервере или ещё где. Теперь мне остаётся сконфигурировать два разных запуска: один –
Всё ещё проблемы с тем, что у меня может быть десяток параметров – как-то некомфортно. А если появится еще вариант для тестирования (dev/preprod/uat/test)?
Да и вообще, раз я храню в гите код тестов, почему бы не хранить и партеры, с которыми я этот код гоняю.
В общем, жизнь показывает, что лучше всего хранить конфиги в файлах .properties.
Файлы
Идея такая: в один файл common.properties я вынесу общие для любого окружения свойства.
common.properties
timeout=10
А в testYa.properties и testYandex.properties положим то, что различается.
testYa.properties
test.url=https://ya.ru
testYandex.properties
test.url=https://yandex.ru
Тест выглядит вот так:
public class test3 { private WebDriver driver; private PropertiesResolver config; @Before public void setUp() { WebDriverManager.chromedriver().setup(); config = new PropertiesResolver(); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(Long.valueOf(config.getProperty("timeout")), TimeUnit.SECONDS); } @Test public void test() { driver.navigate().to(config.getUrl()); driver.findElement(cssSelector("#text")).sendKeys("otus", Keys.ENTER); String text = driver .findElement(cssSelector("[aria-label='Результаты поиска']")) .findElement(cssSelector("li h2")) .getText(); assertEquals(text, "OTUS - Онлайн-образование – Профессиональные курсы"); } @After public void tearDown() { if (driver != null) { driver.quit(); } } }
Только вот класс PropertiesResolver нужно реализовать самому:
class PropertiesResolver { private final Properties properties = new Properties(); PropertiesResolver() { String commonPropsFile = "common.properties"; String envPropsFile = System.getProperty("propertiesFile"); loadProperties(commonPropsFile); loadProperties(envPropsFile); } String getProperty(String name) { return properties.getProperty(name); } URL getUrl(){ try { return new URL(properties.getProperty("test.url")); } catch (MalformedURLException e) { e.printStackTrace(); } return null; } private void loadProperties(String fileName) { InputStream stream = getClass().getClassLoader().getResourceAsStream(fileName); try { properties.load(stream); } catch (IOException e) { e.printStackTrace(); } } }
Здесь мы говорим, что хотим выгрузить все свойства из файла common.properties и из файла, который будет задан при запуске в параметре
Уже лучше — храним конфиги в файлах. Запускаем всего с одним параметром — именем файла.
Только вот, что неприятно, так это это, что теперь все параметры — это строки. И нужно самому крутиться с тем, чтобы привести параметр к нужному типу:
driver.manage().timeouts().implicitlyWait(Long.valueOf(config.getProperty("timeout")), TimeUnit.SECONDS);
Или писать обёртку в классе PropertiesResolver для каждого не String-параметра:
URL getUrl(){ try { return new URL(properties.getProperty("test.url")); } catch (MalformedURLException e) { e.printStackTrace(); } return null; }
Owner
Очень удачно с этим справляется Owner.
Добавим зависимость:
<dependency> <groupId>org.aeonbits.owner</groupId> <artifactId>owner-java8</artifactId> <version>1.0.10</version> </dependency>
И создадим интерфейс, расширяющий Config:
@Sources({"${propertiesFile}", "classpath:common.properties"}) public interface TestConfig extends Config { @DefaultValue("10") long timeout(); @Key("test.url") URL url(); }
Здесь интересны следующие моменты:
@Sources({"${propertiesFile}", "classpath:common.properties"}) — формирует список файлов, откуда читать проперти. Из файла common.properties и из файла, переданного в параметре propertiesFile;- аннотация
@Key() определяет имя параметра, хранящего значение. Аннотация необязательна. Только если имя параметра не совпадает с именем метода в интерфейсе; @DefaultValue — это дефолт вэлью, что тут добавишь?
Теперь тест выглядит вот так:
public class test4 { private WebDriver driver; private TestConfig config; @BeforeClass public static void beforeClass() { WebDriverManager.chromedriver().setup(); } @Before public void setUp() { config = ConfigFactory.create(TestConfig.class); driver = new ChromeDriver(); driver.manage().timeouts().implicitlyWait(config.timeout(), TimeUnit.SECONDS); } @Test public void test() { driver.navigate().to(config.url()); driver.findElement(cssSelector("#text")).sendKeys("otus", Keys.ENTER); String text = driver .findElement(cssSelector("[aria-label='Результаты поиска']")) .findElement(cssSelector("li h2")) .getText(); assertEquals(text, "OTUS - Онлайн-образование – Профессиональные курсы"); } @After public void tearDown() { if (driver != null) { driver.quit(); } } }
Приведение к нужным типам происходит автоматически — не нужно на это отвлекаться. Все переменные вынесены в конфиг-файлы — их удобно хранить, читать, редактировать и версионировать.
Вот как-то так. Удобный инструмент для работы с конфигами. Инструмент стоит того, чтобы добавить его в свою копилку полезных инструментов и, как минимум, почитать, что он ещё может.