Spring забезпечує інтеграцію між MockMvc та HtmlUnit. Це спрощує проведення наскрізного тестування під час використання подання з урахуванням HTML. Така інтеграція дозволяє:

  • Легко тестувати HTML-сторінки за допомогою таких інструментів, як HtmlUnit, WebDriver та Geb, без необхідності розгортання в контейнері сервлетів.

  • Тестувати JavaScript всередині сторінок.

  • Опційно використовувати імітовані служби для прискорення тестування.

  • Забезпечувати спільне використання логіки наскрізними тестами в контейнері та інтеграційними тестами поза контейнером.

MockMvc працює на технологіях шаблонизації, які не залежать від контейнера сервлетів (наприклад, Thymeleaf, FreeMarker та інші), але він не працює з технологією JSP, оскільки вона залежить від контейнера сервлетів. Навіщо потрібна інтеграція HtmlUnit? Відповідь найпростіше знайти, якщо вивчити наочний приклад програми. Припустимо, що у тебе є вебдодаток на Spring MVC, який підтримує CRUD-операції над об'єктом Message. Програма також підтримує сторінкову організацію всіх повідомлень. Як його тестувати?

За допомогою Spring MVC Test можна легко перевірити, чи можемо ми створити Message, таким чином:

Java

MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param("summary", "Spring Rocks")
        .param("text", "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin

@Test
fun test() {
    mockMvc.post("/messages/") {
        param("summary", "Spring Rocks")
        param("text", "In case you didn't know, Spring Rocks!")
    }.andExpect {
        status().is3xxRedirection()
        redirectedUrl("/messages/123")
    }
}

Що якщо нам потрібно протестувати подання форми, що дозволяє створити повідомлення? Наприклад, наприклад, наша форма виглядає як наступний фрагмент:


<form id="messageForm" action="/messages/" method="post">
    <div class="pull-right"><a href="/messages/">Messages</a>
    </div>
    <label for="summary">Summary</label>
    <input type="text" class="required" id="summary" name="summary" value="" />
    <label for="text">Message</label>
    <textarea id="text" name="text"></textarea>
    <div class="form-actions">
        <input type="submit" value="Create" />
    </div>
</form>

Як зробити так, щоб наша форма видавала правильний запит на створення нового повідомлення? Наївна спроба може виглядати так:

Java

mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='summary']").exists())
        .andExpect(xpath("//textarea[@name='text']").exists());
Kotlin

mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='summary']") { exists() }
    xpath("//textarea[@name='text']") { exists() }
}

У цього тесту є кілька очевидних недоліків. Якщо ми оновимо наш контролер, щоб використовувати параметр message замість text, наш тест форми буде пройдено, навіть якщо HTML-форма не синхронізована з контролером. Щоб вирішити цю проблему, можна об'єднати два наших тести так:

Java

String summaryParamName = "summary";
String textParamName = "text";
mockMvc.perform(get("/messages/form"))
        .andExpect(xpath("//input[@name='" + summaryParamName + "']").exists())
        .andExpect(xpath("//textarea[@name='" + textParamName + "']").exists());
MockHttpServletRequestBuilder createMessage = post("/messages/")
        .param(summaryParamName, "Spring Rocks")
        .param(textParamName, "In case you didn't know, Spring Rocks!");
mockMvc.perform(createMessage)
        .andExpect(status().is3xxRedirection())
        .andExpect(redirectedUrl("/messages/123"));
Kotlin

val summaryParamName = "summary";
val textParamName = "text";
mockMvc.get("/messages/form").andExpect {
    xpath("//input[@name='$summaryParamName']") { exists() }
    xpath("//textarea[@name='$textParamName']") { exists() }
}
mockMvc.post("/messages/") {
    param(summaryParamName, "Spring Rocks")
    param(textParamName, "In case you didn't know, Spring Rocks!")
}.andExpect {
    status().is3xxRedirection()
    redirectedUrl("/messages/123")
}

Це знизить ризик некоректного проходження нашого тесту, але все одно залишаються деякі проблеми:

  • Що робити, якщо на сторінці кілька форм? Звісно, ми можемо оновити наші вирази XPath, але вони стають все складнішими, оскільки ми враховуємо все більше факторів: Чи є поля правильного типу? Чи активовані поля? Тощо.

  • Інша проблема полягає в тому, що ми виконуємо вдвічі більше роботи, ніж очікувалося. Нам потрібно спочатку перевірити подання, а потім відправити це подання з тими ж параметрами, які ми щойно перевірили. В ідеалі все це можна зробити відразу.

  • Зрештою, ми все ще не можемо врахувати деякі речі. Наприклад, якщо форма має валідацію JavaScript, яку ми також хочемо протестувати?

Загальна проблема полягає в тому, що тестування вебсторінки не передбачає одноразової взаємодії. Навпаки, це поєднання того, як користувач взаємодіє з вебсторінкою та як ця вебсторінка взаємодіє з іншими ресурсами. Наприклад, результат подання форми використовується як вхідні дані для користувача при створенні повідомлення. До того ж, наше подання форми може потенційно використовувати додаткові ресурси, які впливають на логіку роботи сторінки, наприклад, валідацію JavaScript.

Інтеграційне тестування на допомогу?

Для вирішення згаданих вище проблем можна провести наскрізне інтеграційне тестування, але це має низку недоліків. Розглянемо тестування подання, що дозволяє нам переглядати повідомлення. Нам може знадобитися протестувати таке:

  • Чи відображає наша сторінка повідомлення для користувача про те, що результати відсутні, якщо повідомлення порожні?

  • Чи правильно наша сторінка відображає одне окреме повідомлення?

  • Чи підтримує наша сторінка належним чином сторінкову організацію (пейджинг)?

  • Щоб налаштувати ці тести, переконайся, що наша база даних містить необхідні повідомлення. Це призводить до виникнення низки додаткових проблем: забезпечення наявності в базі даних потрібних повідомлень може бути трудомістким процесом. (З огляду на обмеження зовнішнього ключа).

  • Тестування може сповільнитися, оскільки кожному тесту потрібно переконатися, що база даних знаходиться у належному стані.

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

  • Виконання тверджень для таких елементів, як ідентифікатори, що автоматично генеруються, тимчасові мітки та ін., може бути важко.

Ці проблеми не означають, що необхідно повністю відмовитися від наскрізного інтеграційного тестування. Натомість ми можемо скоротити кількість наскрізних інтеграційних тестів, здійснивши рефакторинг наших деталізованих тестів, щоб використовувати імітовані служби, які працюють набагато швидше, надійніше та без побічних ефектів. Потім можна провести невелику кількість справжніх наскрізних інтеграційних тестів, які перевірять прості робочі процеси, щоб переконатися, що все працює належним чином.

Введення інтеграції HtmlUnit

Як досягти балансу між тестуванням взаємодії наших сторінок та збереженням належної продуктивності в нашому тестовому комплекті? Відповідь: "Шляхом інтеграції MockMvc з HtmlUnit".

Параметри інтеграції HtmlUnit

Якщо необхідно інтегрувати MockMvc з HtmlUnit, є кілька варіантів:

  • MockMvc та HtmlUnit: Використовуй цей варіант, якщо хочеш використовувати сирі бібліотеки HtmlUnit.

  • MockMvc та WebDriver: Використовуйте цей варіант для полегшення розробки та повторного використання коду між фазами інтеграції та наскрізного тестування.

  • MockMvc та Geb: Використовуй цей варіант, якщо необхідно використовувати Groovy для тестування, полегшити розробку та повторно використовувати код між фазами інтеграції та наскрізного тестування.

MockMvc та HtmlUnit

У цьому розділі описано, як інтегрувати MockMvc та HtmlUnit. Використовуй цей варіант, якщо хочеш задіяти сирі бібліотеки HtmlUnit.

Налаштування MockMvc та HtmlUnit

По-перше, переконайся, що ти активував тестову залежність від net.sourceforge.htmlunit: htmlunit. Щоб використовувати HtmlUnit з Apache HttpComponents 4.5+, необхідно використовувати HtmlUnit 2.18 або вище.

Можна легко створити WebClient з HtmlUnit, який буде інтегрований з MockMvc, використовуючи MockMvcWebClientBuilder, як це показано нижче:

Java

WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin

WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Це простий приклад використання MockMvcWebClientBuilder . Для розширеного використання див. розділ "Розширене використанняMockMvcWebClientBuilder".

Таким чином можна гарантувати, що будь-яка URL-адреса, яка посилається на localhost як сервер, буде спрямована на наш екземпляр MockMvc без необхідності в реальному HTTP-з'єднанні. Запит будь-якої іншої URL-адреси відбувається за допомогою мережного підключення, як завжди. Це дозволяє нам легко тестувати використання мереж доставки (і дистрибуції) вмісту (CDN). Тепер ми можемо стандартно використовувати HtmlUnit, але без необхідності розгортання нашої програми в контейнері сервлетів. Наприклад, можна запросити подання створити повідомлення таким чином:

Java
HtmlPage createMsgFormPage = webClient.getPage("http://localhost/messages/form");
Kotlin
val createMsgFormPage = webClient.getPage("http://localhost/messages/form")
Шлях до контексту за замовчуванням — "". До того ж, можна вказати шлях до контексту, як це описано в розділі "Розширене використання MockMvcWebClientBuilder".

Отримавши посилання на HtmlPage, ми одразу можемо заповнити форму та відправити її для створення повідомлення, як це показано в наступному прикладі:

Java

HtmlForm form = createMsgFormPage.getHtmlElementById("messageForm");
HtmlTextInput summaryInput = createMsgFormPage.getHtmlElementById("summary");
summaryInput.setValueAttribute("Spring Rocks");
HtmlTextArea textInput = createMsgFormPage.getHtmlElementById("text");
textInput.setText("In case you didn't know, Spring Rocks!");
HtmlSubmitInput submit = form.getOneHtmlElementByAttribute("input", "type", "submit");
HtmlPage newMessagePage = submit.click();
Kotlin

val form = createMsgFormPage.getHtmlElementById("messageForm")
val summaryInput = createMsgFormPage.getHtmlElementById("summary")
summaryInput.setValueAttribute("Spring Rocks")
val textInput = createMsgFormPage.getHtmlElementById("text")
textInput.setText("In case you didn't know, Spring Rocks!")
val submit = form.getOneHtmlElementByAttribute("input", "type", "submit")
val newMessagePage = submit.click()

Нарешті, можна переконатися, що нове повідомлення було успішно створено. Наступні твердження використовують бібліотеку AssertJ:

Java
 
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123");
String id = newMessagePage.getHtmlElementById("id").getTextContent();
assertThat(id).isEqualTo("123");
String summary = newMessagePage.getHtmlElementById("summary").getTextContent();
assertThat(summary).isEqualTo("Spring Rocks");
String text = newMessagePage.getHtmlElementById("text").getTextContent();
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!");
Kotlin
 
assertThat(newMessagePage.getUrl().toString()).endsWith("/messages/123")
val id = newMessagePage.getHtmlElementById("id").getTextContent()
assertThat(id).isEqualTo("123")
val summary = newMessagePage.getHtmlElementById("summary").getTextContent()
assertThat(summary).isEqualTo("Spring Rocks")
val text = newMessagePage.getHtmlElementById("text").getTextContent()
assertThat(text).isEqualTo("In case you didn't know, Spring Rocks!")

Попередній код покращує наш тест MockMvc кількома способами. По-перше, більше не потрібно явно перевіряти нашу форму, а потім створювати запит, який виглядає як форма. Натомість запитуємо форму, заповнюємо її та відправляємо, тим самим значно знижуючи затримку через обробку.

Ще одним важливим фактором є те, що HtmlUnit використовує двигун Mozilla Rhino для обчислення результатів роботи JavaScript. Це означає, що можна також тестувати логіку роботи JavaScript на наших сторінках.

Додаткову інформацію про використання HtmlUnit див. у документації з використання HtmlUnit.

Розширене використання MockMvcWebClientBuilder

Досі в прикладах ми використовували MockMvcWebClientBuilder найпростішим способом, створюючи WebClient на основі WebApplicationContext , завантаженого для нас Spring TestContext Framework. Цей підхід повторений у наступному прикладі:

Java

WebClient webClient;
@BeforeEach
void setup(WebApplicationContext context) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin

lateinit var webClient: WebClient
@BeforeEach
fun setup(context: WebApplicationContext) {
    webClient = MockMvcWebClientBuilder
            .webAppContextSetup(context)
            .build()
}

Ми також можемо вказати додаткові параметри конфігурації, як показано в наступному прикладі:

Java

WebClient webClient;
@BeforeEach
void setup() {
    webClient = MockMvcWebClientBuilder
        // демонструє застосування MockMvcConfigurer (Spring Security).
        webAppContextSetup(context, springSecurity())
        лише для прикладу - за замовчуванням ""
        .contextPath("")
        // За умовчанням MockMvc використовується тільки для localhost;
        // наступна частина буде використовувати MockMvc для example.com і example.org
        .useMockMvcForHosts("example.com","example.org")
        .build();
}
Kotlin

lateinit var webClient: WebClient
@BeforeEach
fun setup() {
    webClient = MockMvcWebClientBuilder
        // демонструє застосування MockMvcConfigurer (Spring Security)
        .webAppContextSetup(context, springSecurity())
        // тільки для прикладу - за замовчуванням ""
        .contextPath("")
        // За умовчанням MockMvc використовується тільки для localhost;
        // наступна частина буде використовувати MockMvc для example.com і example.org
        .useMockMvcForHosts("example.com","example.org") .build() }

Як альтернативу можна виконати таке ж налаштування, налаштувавши екземпляр MockMvc окремо і передавши його до MockMvcWebClientBuilder, як показано нижче:

Java

MockMvc mockMvc = MockMvcBuilders
    .webAppContextSetup(context)
    .apply(springSecurity())
    .build();
webClient = MockMvcWebClientBuilder
        .mockMvcSetup(mockMvc)
        // тільки для прикладу – за замовчуванням ""
        .contextPath("")
        // За умовчанням MockMvc використовується тільки для localhost;
        // наступна частина буде використовувати MockMvc для example.com і example.org
        .useMockMvcForHosts("example.com","example.org")
        .build()
Kotlin
// Неможливо в Kotlin,поки https://youtrack.jetbrains.com/issue/KT-22208 не буде виправлено

Це більш перевантажений код, але, створюючи WebClient за допомогою екземпляра MockMvc, ми отримуємо весь функціонал MockMvc у своє розпорядження.

Додаткову інформацію про створення екземпляра MockMvc можна знайти в розділі "Параметри налаштування".

MockMvc та WebDriver

У попередніх розділах ми ознайомилися з використанням MockMvc у поєднанні з сирими API-інтерфейсами HtmlUnit. У цьому розділі використані додаткові абстракції у Selenium WebDriver, що робить все ще простіше.

Чому WebDriver і MockMvc?

Ми вже можемо використовувати HtmlUnit і MockMvc, то навіщо нам WebDriver? Selenium WebDriver надає надзвичайно витончений API-інтерфейс, який дозволить нам легко організувати наш код. Щоб краще показати, як це працює, в цьому розділі ми розглянемо приклад. Не дивлячись на те, що WebDriver є частиною Selenium, він не вимагає наявності Selenium Server для запуску ваших тестів.

Припустимо, нам потрібно переконатися, що повідомлення створено правильно. Тести включають пошук елементів введення HTML-форми, їх заповнення та виконання різних тверджень.

Такий підхід виливається в численні окремі тести, оскільки нам потрібно перевірити також і умови виникнення помилок. Наприклад, нам потрібно, щоб при заповненні частини форми видавалась помилка. Якщо ми заповнимо всю форму, то після цього має з'явитися новостворене повідомлення.

Якби одне з полів було названо "summary", ми могли б отримати щось схоже на те, що наведено нижче, що повторюється в кількох місцях. у наших тестах:

Java

HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
summaryInput.setValueAttribute(summary);
Kotlin

val summaryInput = currentPage.getHtmlElementById("summary")
summaryInput .setValueAttribute(summary)

Що станеться, якщо ми змінимо id на smmry? Це змусить нас оновити всі наші тести, щоб врахувати цю зміну. Це порушує принцип "не повторюйся" (DRY), тому в ідеалі ми повинні виділити цей код окремим методом, як показано нижче:

Java

public HtmlPage createMessage( HtmlPage currentPage, String summary, String text) {
    setSummary(currentPage, summary);
    // ...
}
public void setSummary(HtmlPage currentPage, String summary) {
    HtmlTextInput summaryInput = currentPage.getHtmlElementById("summary");
    summaryInput.setValueAttribute(summary);
}
Kotlin

fun createMessage(currentPage: HtmlPage, summary:String, text:String) :HtmlPage{
    setSummary(currentPage, summary);
    // ...
}
fun setSummary(currentPage:HtmlPage , summary: String) {
    val summaryInput = currentPage.getHtmlElementById("summary")
    summaryInput.setValueAttribute(summary)
}
div>

Так нам гарантовано не доведеться оновлювати всі наші тести, якщо ми змінимо інтерфейс користувача.

Можна навіть піти ще далі і помістити цю логіку в Object, що представляє HtmlPage, на якій ми зараз знаходимося, як показано в наступному прикладі:

Java

public class CreateMessagePage {
    final HtmlPage currentPage;
    final HtmlTextInput summaryInput;
    final HtmlSubmitInput submit;
    public CreateMessagePage(HtmlPage currentPage) {
        this.currentPage = currentPage;
        this.summaryInput = currentPage.getHtmlElementById("summary");
        this.submit = currentPage.getHtmlElementById("submit");
    }
    public <T> T createMessage(String summary, String text) throws Exception {
        setSummary(summary);
        HtmlPage result = submit.click();
        boolean error = CreateMessagePage.at(result);
        return (T) (error ? new CreateMessagePage(result) : new ViewMessagePage(result));
    }
    public void setSummary(String summary) throws Exception {
        summaryInput.setValueAttribute(summary);
    }
    public static boolean at(HtmlPage page) {
        return "Create Message".equals(page.getTitleText());
    }
}
Kotlin

class CreateMessagePage(private val currentPage: HtmlPage) {
        val summaryInput: HtmlTextInput = currentPage.getHtmlElementById("summary")
        val submit: HtmlSubmitInput = currentPage.getHtmlElementById("submit")
        fun <T> createMessage(summary: String, text: String): T {
            setSummary(summary)
            val result = submit.click()
            val error = at(result)
            return (if (error) CreateMessagePage(result) else ViewMessagePage(result)) as T
        }
        fun setSummary(summary: String) {
            summaryInput.setValueAttribute(summary)
        }
        fun at(page: HtmlPage): Boolean {
            return "Create Message" == page.getTitleText()
        }
    }
}

Раніше цей шаблон був відомий як шаблон об'єкта сторінки (Page Object Pattern ). Звичайно, можна зробити це і за допомогою HtmlUnit, але WebDriver надає деякі інструменти, які значно спрощують реалізацію цього шаблону, і ми розглянемо їх у наступних розділах.

Налаштування MockMvc та WebDriver

Щоб використовувати Selenium WebDriver з фреймворком Spring MVC Test, переконайся, що твій проєкт містить тестову залежність від org.seleniumhq.selenium:selenium-htmlunit-driver.

Можна легко створити Selenium WebDriver, який інтегрується з MockMvc, використовуючи MockMvcHtmlUnitDriverBuilder, як показано в наступному прикладі:

Java

WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin

lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}
Це простий приклад використання MockMvcHtmlUnitDriverBuilder . Для ознайомлення з можливостями розширеного використання див. розділ " Розширене використання MockMvcHtmlUnitDriverBuilder".

У попередньому прикладі будь-яка URL-адреса, яка посилається на localhost як сервер, гарантовано буде направлена на наш екземпляр MockMvc без необхідності в реальному HTTP-з'єднанні. Запит будь-якої іншої URL-адреси відбувається за допомогою мережного підключення, як завжди. Це дозволяє нам легко тестувати використання мереж доставки (і дистрибуції) вмісту (CDN). сервлетів. Наприклад, можна запросити подання створити повідомлення таким чином:

Java
CreateMessagePage page = CreateMessagePage.to(driver);
Kotlin
val page = CreateMessagePage.to(driver)

Потім можна заповнити форму та відправити її, щоб створити повідомлення, наступним чином:


ViewMessagePage viewMessagePage =
        page.createMessage(ViewMessagePage.class, expectedSummary, expectedText);
Kotlin

val viewMessagePage =
    page.createMessage(ViewMessagePage::class, expectedSummary, expectedText)

Це покращить структуру нашого тесту HtmlUnit за рахунок використання шаблону Page Object Pattern. Як уже було згадано в підрозділі "Чому WebDriver та MockMvc?", ми можемо використовувати шаблон Page Object Pattern з HtmlUnit, але з WebDriver це все ще простіше. Розглянемо наступну реалізацію CreateMessagePage:

Java

public class CreateMessagePage
        extends AbstractPage { 
    
    private WebElement summary;
    private WebElement text;
    
    @FindBy(css = "input[type=submit]")
    private WebElement submit;
    public CreateMessagePage(WebDriver driver) {
        super(driver);
    }
    public <T> T createMessage(Class<T> resultPage, String summary, String details) {
        this.summary.sendKeys(summary);
        this.text.sendKeys(details);
        this.submit.click();
        return PageFactory.initElements(driver, resultPage);
    }
    public static CreateMessagePage to(WebDriver driver) {
        driver.get("http://localhost:9990/mail/messages/form");
        return PageFactory.initElements(driver, CreateMessagePage.class);
    }
}        
  1. CreateMessagePage розширює AbstractPage. Не будемо детально зупинятися на AbstractPage, але, якщо коротко, вона містить спільну функціональність для всіх наших сторінок. Наприклад, якщо в нашому додатку є навігаційна панель, глобальні повідомлення про помилки та інші функції, ми можемо розмістити цю логіку в загальному місці.
  2. У нас є змінна член для кожної частини HTML-сторінки, яка нас цікавить. Вони мають тип WebElement. PageFactory з WebDriver дозволяє нам прибрати багато зайвого коду з HtmlUnit- версії CreateMessagePage, автоматично дозволивши кожен WebElement. Метод PageFactory#initElements(WebDriver,Class<T>) автоматично дозволяє кожен WebElement, використовуючи ім'я поля та здійснюючи його пошук за id або name елемента в HTML-сторінці.
  3. Можна використовувати анотацію @FindBy щоб перевизначити логіку пошуку за замовчуванням. У нашому прикладі показано, як використовувати анотацію @FindBy для пошуку нашої кнопки відправки за допомогою селектора css(input[type=submit]).
Kotlin

class CreateMessagePage(private val driver: WebDriver) : AbstractPage(driver) { 
    
    private lateinit var summary: WebElement
    private lateinit var text: WebElement
    
    @FindBy(css = "input[type=submit]")
    private lateinit var submit: WebElement
    fun <T> createMessage(resultPage: Class<T>, summary: String, details: String): T {
        this.summary.sendKeys(summary)
        text.sendKeys(details)
        submit.click()
        return PageFactory.initElements(driver, resultPage)
    }
    companion object {
        fun to(driver: WebDriver): CreateMessagePage {
            driver.get("http://localhost:9990/mail/messages/form")
            return PageFactory.initElements(driver, CreateMessagePage::class.java)
        }
    }
}    
  1. CreateMessagePage розширює AbstractPage. Не будемо детально зупинятися на AbstractPage, але, якщо коротко, вона містить спільну функціональність для всіх наших сторінок. Наприклад, якщо в нашому додатку є навігаційна панель, глобальні повідомлення про помилки та інші функції, ми можемо розмістити цю логіку в загальному місці.
  2. У нас є змінна член для кожної частини HTML-сторінки, яка нас цікавить. Вони мають тип WebElement. PageFactory з WebDriver дозволяє нам прибрати багато зайвого коду з HtmlUnit- версії CreateMessagePage, автоматично дозволивши кожен WebElement. Метод PageFactory#initElements(WebDriver,Class<T>) автоматично дозволяє кожен WebElement, використовуючи ім'я поля та здійснюючи його пошук за id або name елемента в HTML-сторінці.
  3. Можна використовувати анотацію @FindBy щоб перевизначити логіку пошуку за замовчуванням. У нашому прикладі показано, як використовувати інструкцію @FindBy для пошуку нашої кнопки відправки за допомогою селектора css(input[type=submit]).

Зрештою, можна переконатися, що нове повідомлення було успішно створено. Наступні твердження використовують бібліотеку тверджень AssertJ:

Java

assertThat(viewMessagePage.getMessage()).isEqualTo(expectedMessage);
assertThat(viewMessagePage.getSuccess()).isEqualTo("Successfully created a new message");
Kotlin

assertThat(viewMessagePage.message).isEqualTo(expectedMessage)
assertThat(viewMessagePage.success).isEqualTo("Successfully created a new message")

Ми бачимо, що наша сторінка ViewMessagePage дозволяє нам взаємодіяти з нашою спеціальною моделлю предметної галузі. Наприклад, вона відкриває метод, який повертає об'єкт Message:

Java

public Message getMessage() throws ParseException {
    Message message = new Message ();
    message.setId(getId());
    message.setCreated(getCreated());
    message.setSummary(getSummary());
    message.setText(getText());
    return message;
}
Kotlin
fun getMessage() = Message(getId(), getCreated(), getSummary() , getText())

Потім можна використовувати повнофункціональні об'єкти предметної області в наших твердженнях.

Зрештою, не потрібно забувати закривати екземпляр WebDriver після завершення тесту, як показано нижче:

Java

@AfterEach
void destroy() {
    if (driver != null) {
        driver .close();
    }
}
        
Kotlin

@AfterEach
fun destroy() {
    if (driver != null) {
        driver. close()
    }
}
        

Додаткову інформацію про використання WebDriver див. у документації по Selenium WebDriver.

Розширене використання MockMvcHtmlUnitDriverBuilder

Досі в прикладах ми використовували MockMvcHtmlUnitDriverBuilder найпростішим способом, створюючи WebDriver на основі WebApplicationContext, завантаженого для нас Spring TestContext Framework. Цей підхід повторений тут так:

Java

WebDriver driver;
@BeforeEach
void setup(WebApplicationContext context) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build();
}
Kotlin

lateinit var driver: WebDriver
@BeforeEach
fun setup(context: WebApplicationContext) {
    driver = MockMvcHtmlUnitDriverBuilder
            .webAppContextSetup(context)
            .build()
}

Ми також можемо вказати додаткові параметри конфігурації, як показано нижче:

Java

WebDriver driver;
@BeforeEach
void setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // демонструє застосування MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // тільки для прикладу – за умовчанням ""
            .contextPath("")
            // За умовчанням MockMvc використовується тільки для localhost;
            // наступна частина буде використовувати MockMvc для example.com і example.org
            .useMockMvcForHosts("example.com","example.org")
            .build();
}
Kotlin

lateinit var driver: WebDriver
@BeforeEach
fun setup() {
    driver = MockMvcHtmlUnitDriverBuilder
            // демонструє застосування MockMvcConfigurer (Spring Security)
            .webAppContextSetup(context, springSecurity())
            // тільки для прикладу – за умовчанням ""
            .contextPath("")
            // За умовчанням MockMvc використовується тільки для localhost;
            // наступна частина буде використовувати MockMvc для example.com і example.org
            .useMockMvcForHosts("example.com","example.org")
            .build()
}

Як альтернативу можна виконати таке ж налаштування, налаштувавши екземпляр MockMvc окремо і передавши його в MockMvcHtmlUnitDriverBuilder таким чином:

Java

MockMvc mockMvc = MockMvcBuilders
        .webAppContextSetup(context)
        .apply(springSecurity())
        .build();
driver = MockMvcHtmlUnitDriverBuilder
        .mockMvcSetup(mockMvc)
        // тільки для прикладу – за умовчанням ""
        .contextPath("")
        // За умовчанням MockMvc використовується тільки для localhost
        // наступна частина буде використовувати MockMvc для example.com і example.org
        .useMockMvcForHosts("example.com","example.org")
        .build();
Kotlin
// Неможливо в Kotlin,поки https://youtrack.jetbrains.com/issue/KT-22208 не буде виправлено

Це більш перевантажений код, але, створюючи WebDriver за допомогою екземпляра MockMvc, ми отримуємо весь функціонал MockMvc у своєму розпорядженні.

Додаткову інформацію про створення екземпляра MockMvc можна знайти в розділі "Параметри налаштування".

MockMvc та Geb

У попередньому розділі ми розглянули, як використовувати MockMvc з WebDriver. У цьому розділі ми використовуємо Geb, щоб зробити наші тести ще більш Groovy-орієнтованими.

Чому Geb і MockMvc?

Geb підтримується WebDriver, тому він дає багато з тих переваг, які ми одержуємо від WebDriver. Однак Geb ще сильніше все спрощує завдяки тому, що працює зі стереотипним кодом замість нас. Selenium WebDriver, який використовує MockMvc, так:

def setup() {
    browser.driver = MockMvcHtmlUnitDriverBuilder
        .webAppContextSetup(context)
        .build()
}
Це простий приклад використання MockMvcHtmlUnitDriverBuilder. Для ознайомлення з можливостями розширеного використання див. розділ " Розширене використання MockMvcHtmlUnitDriverBuilder".

Таким чином можна гарантувати, що будь-яка URL-адреса, яка посилається на localhost як сервер, буде спрямована на наш екземпляр MockMvc без необхідності в реальному HTTP-з'єднанні. Запит будь-якої іншої URL-адреси відбувається за допомогою мережного підключення, як завжди. Це дозволяє нам легко тестувати використання мереж доставки (і дистрибуції) вмісту (CDN).

Використання MockMvc та Geb

Тепер ми можемо стандартно використовувати Geb, але без необхідності розгортання нашої програми в контейнері сервлетів. Наприклад, можна зробити запит в подання на створення повідомлення таким чином:

to CreateMessagePage

Потім можна заповнити форму та відправити її, щоб створити повідомлення, таким чином:


when:
form.summary = expectedSummary
form.text = expectedMessage
submit.click( ViewMessagePage)
        

Будь-які нерозпізнані виклики методів, звернення до властивостей або посилання, які не були знайдені, передаються поточному об'єкту сторінки. Це дозволяє позбавитися великого обсягу стереотипного коду, необхідного при прямому використанні WebDriver. Як згадувалося раніше, можна використовувати шаблон Page Object Pattern з HtmlUnit та WebDriver, але з Geb все стає ще. Розглянемо нашу нову реалізацію CreateMessagePage на основі Groovy:


class CreateMessagePage extends Page {
    static url = 'messages/form'
    static at = { assert title == 'Messages : Create'; true}
    static content = {
        submit { $('input[type=submit]') }
        form { $('form') }
        errors(required:false) { $('label.error, .alert-error')?.text() }
    }
}

Наш CreateMessagePage розширює AbstractPage. Не будемо докладно зупинятися на Page, але, якщо коротко, вона містить спільну функціональність для всіх наших сторінок. Ми визначаємо URL-адресу, за якою можна знайти цю сторінку. Це дозволяє нам перейти на сторінку, як показано нижче:

to CreateMessagePage

У нас також є замикання at, яке визначає, чи ми знаходимося на вказаній сторінці. Воно має повертати true, якщо ми знаходимося на правильній сторінці. Ось тому ми можемо підтвердити, що знаходимося на правильній сторінці, так:

then:
at CreateMessagePage
errors.contains('This field is required.')
Використовуємо затвердження у замиканні, щоб можна було визначити, де щось пішло не так, якщо ми опинимося на неправильній сторінці.

Далі ми створюємо замикання content, в якому вказуються всі цікаві місця на сторінці. Можна використовувати jQuery-подібний API-інтерфейс навігатора для вибору цікавого для нас вмісту.

Нарешті, ми можемо переконатися, що нове повідомлення було успішно створено, таким чином:


then:
at ViewMessagePage
success == 'Successfully created a new message'
id
date
summary == expectedSummary
message == expectedMessage

Детальнішу інформацію про те, як вичавити максимум з Geb, можна знайти в посібнику користувача під назвою "The Book of Geb".