Я использовал это некоторое время, и я столкнулся с несколькими вещами, которые, кажется, делают жизнь легче. Я думал, что поделюсь этим в качестве учебника, поэтому я проведу вас через эти части:
- Настройка веб-проекта с использованием Maven, настройка Selenium для запуска в качестве интеграционного теста на CI
- Изучите хорошие способы моделирования страниц на вашем сайте, используя «объекты страниц» и другие способы создания точек защищенной вариации.
- Используйте JPA и Hibernate для выполнения операций CRUD над базой данных, и пусть Maven выполнит интеграционные тесты на них без какой-либо дорогостоящей и часто недокументированной установки, которая иногда влечет за собой.
В этом посте предполагается, что вы знакомы с Java, Spring, Maven 2 и, конечно, HTML. Вы также хотите, чтобы Firefox был установлен на вашем компьютере. Этот учебник предназначен для технологической независимости.
Создание веб-приложения
Для начала нам понадобится веб-приложение для тестирования. Создайте проект, используя maven-webapp-archetype, и назовите его «selenuim-tutorial».
Для запуска интеграционных тестов (ИТ) мы будем использовать плагин Cargo. Это запускает и останавливает контейнеры, такие как Jetty и Tomcat. Вы можете использовать Cargo для запуска вашего сайта с помощью Jetty (по умолчанию) в одной команде без каких-либо изменений:
1
|
mvn cargo:run |
И проверьте это в вашем браузере по адресу:
HTTP: // локальный: 8080 / selenuim-учебник
Вы получите 404 без настройки файла приветствия, поэтому добавьте это в файл web.xml:
1
2
3
|
< welcome-file-list > < welcome-file >/index.jsp</ welcome-file > </ welcome-file-list > |
Если вы запускаете груз: бегите снова, теперь вы увидите «Hello World!» страница, созданная Maven.
Конфигурирование груза
Мы можем настроить Cargo на запуск контейнера Jetty до запуска тестов, а затем остановить его. Это позволит нам запустить наш сайт, запустить интеграционные тесты, а затем остановить его.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
< plugin > < groupId >org.codehaus.cargo</ groupId > < artifactId >cargo-maven2-plugin</ artifactId > < version >1.2.0</ version > < executions > < execution > < id >start</ id > < phase >pre-integration-test</ phase > < goals > < goal >start</ goal > </ goals > </ execution > < execution > < id >stop</ id > < phase >post-integration-test</ phase > < goals > < goal >stop</ goal > </ goals > </ execution > </ executions > </ plugin > |
Вы можете проверить эту работу с:
1
|
mvn verify |
Стоит отметить, что Cargo работает на порте 8080. Если у вас уже есть процесс, прослушивающий этот порт, вы можете увидеть ошибку, подобную этой:
1
|
java.net.BindException: Address already in use |
Это может быть потому, что вы уже запускаете другой контейнер на этом порту. Если вы хотите запустить это на CI (который может сам работать на порте 8080), это, вероятно, вы захотите изменить. Добавьте эти строки в настройку плагина:
1
2
3
4
5
6
7
8
|
< configuration > < type >standalone</ type > < configuration > < properties > < cargo.servlet.port >10001</ cargo.servlet.port > </ properties > </ configuration > </ configuration > |
Теперь приложение будет здесь:
HTTP: // локальный: 10001 / selenuim-учебник /
Настройка фазы тестирования интеграции
Далее нам нужно иметь возможность запускать интеграционные тесты. Для этого требуется отказоустойчивый плагин Maven с соответствующими целями, добавленными к вашему пом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
< plugin > < groupId >org.apache.maven.plugins</ groupId > < artifactId >maven-failsafe-plugin</ artifactId > < version >2.12</ version > < executions > < execution > < id >default</ id > < goals > < goal >integration-test</ goal > < goal >verify</ goal > </ goals > </ execution > </ executions > </ plugin > |
По умолчанию Failsafe ожидает, что тесты будут соответствовать шаблону «src / test / java / * / * IT.java». Давайте создадим тест, чтобы продемонстрировать это. Обратите внимание, что я не изменился с Junit 3.8.1 еще. Я объясню почему позже.
Вот базовый, неполный тест:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package tutorial; import junit.framework.TestCase; public class IndexPageIT extends TestCase { @Override protected void setUp() throws Exception { super .setUp(); } @Override protected void tearDown() throws Exception { super .tearDown(); } public void testWeSeeHelloWorld() { fail(); } } |
Тест, который работает:
1
|
mvn verify |
Вы должны увидеть один тестовый сбой.
Чтобы протестировать с помощью Selenium, вам нужно добавить тестовую зависимость в pom.xml:
1
2
3
4
5
6
|
< dependency > < groupId >org.seleniumhq.selenium</ groupId > < artifactId >selenium-firefox-driver</ artifactId > < version >2.19.0</ version > < scope >test</ scope > </ dependency > |
Теперь мы можем внести пару изменений в наш тест:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxDriver; … private URI siteBase; private WebDriver drv; @Override protected void setUp() throws Exception { super.setUp(); drv = new FirefoxDriver(); } ... public void testWeSeeHelloWorld() { drv.get(siteBase.toString()); assertTrue(drv.getPageSource().contains( "Hello World" )); } |
Мы удалим эти жестко закодированные значения позже.
Запустите это снова:
1
|
mvn verify |
Вы не должны видеть никаких сбоев. То, что у вас будет, это долгий Firefox. Это не будет закрыто. Запустите этот тест 100 раз, и у вас будет 100 запущенных Firefox. Это быстро станет проблемой. Мы можем решить эту проблему, добавив этот блок инициализации в наш тест:
1
2
3
4
5
6
7
8
|
{ Runtime.getRuntime().addShutdownHook( new Thread() { @Override public void run() { drv.close(); } }); } |
Естественно, если мы создадим еще один тест, мы скоро нарушим принципы DRY. Мы вернемся к этому в следующей части, а также рассмотрим, что происходит, когда нам требуется соединение с базой данных, и некоторые другие способы убедиться, что ваши тесты просты в написании и просты в обслуживании.
Весенний контекст
В предыдущем примере URI для приложения и используемый драйвер были жестко запрограммированы. Предполагая, что вы знакомы с контекстом Spring, изменить их довольно просто. Сначала мы добавим правильные зависимости:
1
2
3
4
5
6
|
< dependency > < groupId >org.springframework</ groupId > < artifactId >spring-context</ artifactId > < version >3.1.1.RELEASE</ version > < scope >test</ scope > </ dependency > |
Это позволит нам использовать и контекст приложения для внедрения зависимостей. Но нам также потребуется правильный бегун Junit для проверки этого, который можно найти в пакете spring-test:
1
2
3
4
5
6
|
< dependency > < groupId >org.springframework</ groupId > < artifactId >spring-test</ artifactId > < version >3.1.1.RELEASE</ version > < scope >test</ scope > </ dependency > |
Теперь мы можем обновить наш тест, чтобы использовать это. Сначала нам нужно создать src / test / resources / applicationContext-test.xml
01
02
03
04
05
06
07
08
09
10
11
12
|
<? xml version = "1.0" encoding = "UTF-8" ?> xsi:schemaLocation="http://www.springframework.org/schema/beans < bean id = "siteBase" class = "java.net.URI" > </ bean > < bean id = "drv" class = "org.openqa.selenium.firefox.FirefoxDriver" destroy-method = "quit" /> </ beans > |
Spring завершит очистку браузера после его завершения, поэтому мы можем убрать хук отключения из AbstractIT. Это более надежно, чем тестовый пример.
Spring-тест не работает с JUnit 3, ему нужен как минимум JUnit 4.5. Обновление до версии 4.10 в нашем файле pom.xml:
1
2
3
4
5
6
|
< dependency > < groupId >junit</ groupId > < artifactId >junit</ artifactId > < version >4.10</ version > < scope >test</ scope > </ dependency > |
Наконец, нам нужно обновить наш тест для работы с Spring и JUnit 4.x:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
package tutorial; import static org.junit.Assert.assertTrue; import java.net.URI; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = { "/applicationContext-test.xml" }) public class IndexPageIT { @Autowired private URI siteBase; @Autowired private WebDriver drv; @Test public void testWeSeeHelloWorld() { ... |
Эти изменения переместили конфигурацию из жестко закодированных значений в конфигурацию XML. Теперь мы можем изменить местоположение, которое мы тестируем, например, на другой хост, и изменить используемый веб-драйвер, который оставляется для пользователя в качестве упражнения.
Небольшая заметка о браузерах. Я обнаружил, что после обновления браузера тесты часто начинают проваливаться. Кажется, есть два решения для этого:
- Обновите до последней версии веб-драйвера.
- Не обновляйте браузер.
Я подозреваю, что первый вариант является лучшим в большинстве случаев по соображениям безопасности
Абстрактная ИТ
В настоящее время вам необходимо продублировать весь код IoC. Простой рефакторинг может разобраться в этом. Мы создадим суперкласс для всех тестов и общие функции. Этот рефакторинг использует наследование, а не композицию, по причинам, о которых я расскажу позже.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
package tutorial; import java.net.URI; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; @RunWith (SpringJUnit4ClassRunner. class ) @ContextConfiguration (locations = { "/applicationContext-test.xml" }) public abstract class AbstractIT { @Autowired private URI siteBase; @Autowired private WebDriver drv; public URI getSiteBase() { return siteBase; } public WebDriver getDrv() { return drv; } } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
package tutorial; import static org.junit.Assert.assertTrue; import org.junit.Test; public class IndexPageIT extends AbstractIT { @Test public void testWeSeeHelloWorld() { getDrv().get(getSiteBase().toString()); assertTrue(getDrv().getPageSource().contains( "Hello World" )); } } |
Объекты страницы
«Объект страницы» — это объект, который инкапсулирует один экземпляр страницы и предоставляет программный API для этого экземпляра. Основная страница может быть:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package tutorial; import java.net.URI; import org.openqa.selenium.WebDriver; public class IndexPage { /** * @param drv * A web driver. * @param siteBase * The root URI of a the expected site. * @return Whether or not the driver is at the index page of the site. */ public static boolean isAtIndexPage(WebDriver drv, URI siteBase) { return drv.getCurrentUrl().equals(siteBase); } private final WebDriver drv; private final URI siteBase; public IndexPage(WebDriver drv, URI siteBase) { if (!isAtIndexPage(drv, siteBase)) { throw new IllegalStateException(); } this .drv = drv; this .siteBase = siteBase; } } |
Обратите внимание, что я предоставил статический метод для возврата, находимся ли мы на странице индекса или нет, и прокомментировал его (без необходимости, для такого самодокументируемого метода); объекты страницы формируют API и могут быть полезны для документирования. Вы также увидите, что мы генерируем исключение, если URL неверен. Стоит рассмотреть, какое условие вы используете для идентификации страниц. Все, что может измениться (например, заголовок страницы, который может меняться в зависимости от языка), вероятно, плохой выбор. Что-то неизменное и машиночитаемое (например, путь страницы) — хороший выбор; если вы хотите изменить путь, то вам нужно изменить тест.
Теперь давайте создадим себе проблему. Я хотел бы добавить это в index.jsp, но полученный HTML-код не разбирается:
1
|
<% throw new RuntimeException(); %> |
Вместо этого мы создадим новый сервлет, но сначала нам нужно добавить servlet-api в pom.xml:
1
2
3
4
5
6
|
< dependency > < groupId >javax.servlet</ groupId > < artifactId >servlet-api</ artifactId > < version >2.5</ version > < scope >provided</ scope > </ dependency > |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
package tutorial; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class IndexServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { throw new RuntimeException(); } } |
Добавьте его в web.xml и удалите ненужную страницу приветствия:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
<! DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" < web-app > < servlet > < servlet-name >IndexServlet</ servlet-name > < servlet-class >tutorial.IndexServlet</ servlet-class > </ servlet > < servlet-mapping > < servlet-name >IndexServlet</ servlet-name > < url-pattern >/</ url-pattern > </ servlet-mapping > </ web-app > |
Обновить IndexPageIT:
1
2
3
4
5
6
|
@Test public void testWeSeeHelloWorld() { getDrv().get(getSiteBase().toString()); new IndexPage(getDrv(), getSiteBase()); } |
Запустите тест снова. Это проходит. Это может быть не то поведение, которое вы хотите. Selenium не предоставляет способ проверки кода состояния HTTP через экземпляр WebDriver. Кроме того, страница ошибок по умолчанию недостаточно согласована между контейнерами (сравните это с тем, что происходит, если вы, например, запускаете Tomcat); мы не можем делать предположения о содержании страницы ошибки, чтобы выяснить, произошла ли ошибка.
Наша страница индекса в настоящее время не имеет каких-либо машиночитаемых функций, которые позволяют нам отличить ее от страницы ошибки.
Чтобы привести в порядок, измените сервлет для отображения index.jsp:
1
2
3
|
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { getServletContext().getRequestDispatcher( "/index.jsp" ).forward(request, response); } |
В настоящее время index.jsp слишком прост. Создайте новую страницу с именем create-order.jsp вместе с index.jsp и создайте ссылку на index.jsp на эту страницу. Мы можем создать новый класс для страницы заказа и метод, который перемещает нас со страницы индекса на страницу заказа.
Добавьте следующее в index.jsp:
1
|
< a href = "create-order.jsp" >Create an order</ a > |
create-order.jsp может быть пустым на данный момент. Мы также можем создать объект страницы для него:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
package tutorial; import java.net.URI; import org.openqa.selenium.WebDriver; public class CreateOrderPage { public static boolean isAtCreateOrderPage(WebDriver drv, URI siteBase) { return drv.getCurrentUrl().equals(siteBase.toString() + "create-order.jsp" ); } private final WebDriver drv; private final URI siteBase; public CreateOrderPage(WebDriver drv, URI siteBase) { if (!isAtCreateOrderPage(drv, siteBase)) { throw new IllegalStateException(); } this .drv = drv; this .siteBase = siteBase; } } |
Добавьте следующую зависимость в pom.xml, которая даст нам несколько полезных аннотаций:
1
2
3
4
5
6
|
< dependency > < groupId >org.seleniumhq.selenium</ groupId > < artifactId >selenium-support</ artifactId > < version >2.19.0</ version > < scope >test</ scope > </ dependency > |
Теперь мы можем конкретизировать IndexPage:
1
2
3
4
5
6
7
8
9
|
@FindBy (css = "a[href='create-order.jsp']" ) private WebElement createOrderLink; public IndexPage(WebDriver drv, URI siteBase) { if (!isAtIndexPage(drv, siteBase)) { throw new IllegalStateException(); } PageFactory.initElements(drv, this ); this .drv = drv; this .siteBase = siteBase; } |
Этот вызов PageFactory.initElements заполнит поля, аннотированные @FindBy, объектом, соответствующим элементу на веб-странице. Обратите внимание на использование селектора CSS, он предназначен для ссылки таким образом, что вряд ли изменится. Другие методы включают сопоставление элементов на странице с использованием текста ссылки (который может изменяться для разных языков).
Теперь мы можем создать метод на IndexPages, который переходит к CreateOrderPages.
1
2
3
4
|
public CreateOrderPage createOrder() { createOrderLink.click(); return new CreateOrderPage(drv, siteBase); } |
Наконец, мы можем создать тест для этой ссылки в IndexPageIT:
1
2
3
4
5
6
7
8
|
@Test public void testCreateOrder() { getDrv().get(getSiteBase().toString()); new IndexPage(getDrv(), getSiteBase()).createOrder(); assertTrue(CreateOrderPage.isAtCreateOrderPage(getDrv(), getSiteBase())); } |
Выполните mvn verify, и вы должны найти новые тесты. На данный момент у нас есть два теста, которые не очищаются между ними. Они используют один и тот же экземпляр WebDriver для обоих тестов, последняя страница будет по-прежнему открыта, а все установленные файлы cookie останутся такими. Есть плюсы и минусы создания одного экземпляра WebDriver для нескольких тестов. Основным преимуществом является сокращение затрат времени на открытие и закрытие браузеров, но в том-то и дело, что браузер фактически остается грязным после каждого теста, установки файлов cookie, открытия всплывающих окон. Мы можем убедиться, что он чист перед каждым тестом с помощью подходящего метода setUp в AbstractIT:
1
2
3
4
5
|
@Before public void setUp() { getDrv().manage().deleteAllCookies(); getDrv().get(siteBase.toString()); } |
Есть альтернативные подходы к этому, я оставляю вам возможность искать способы создания нового экземпляра WebDriver перед каждым тестом.
Аннотация @FindBy особенно полезна при использовании в формах. Добавьте новую форму в create-order.jsp:
1
2
3
4
5
|
< form method = "post" name = "create-order" > Item: < input name = "item" /> < br /> Amount: < input name = "amount" />< br /> < input type = "submit" /> </ form > |
Добавьте эти WebElements в CreateOrderPage и метод для отправки формы:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@FindBy (css = "form[name='create-order'] input[name='item']" ) private WebElement itemInput; @FindBy (css = "form[name='create-order'] input[name='amount']" ) private WebElement amountInput; @FindBy (css = "form[name='create-order'] input[type='submit']" ) private WebElement submit; public CreateOrderPage(WebDriver drv, URI siteBase) { if (!isAtCreateOrderPage(drv, siteBase)) { throw new IllegalStateException(); } PageFactory.initElements(drv, this ); this .drv = drv; this .siteBase = siteBase; } public CreateOrderPage submit(String item, String amount) { itemInput.sendKeys(item); amountInput.sendKeys(amount); submit.click(); return new CreateOrderPage(drv, siteBase); } |
Наконец, мы можем создать тест для этого:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
package tutorial; import static org.junit.Assert.*; import org.junit.Test; public class CreateOrderPageIT extends AbstractIT { @Test public void testSubmit() { new IndexPage(getDrv(), getSiteBase()).createOrder().submit( "foo" , "1.0" ); } } |
Вывод
Вы можете заметить, что метод submit не требует, чтобы сумма была числом, как вы могли ожидать. Вы можете создать тест, чтобы увидеть, что отправка строки вместо числа. Интеграционные тесты могут быть трудоемкими для написания и уязвимыми для взлома в результате изменений таких вещей, как ID элемента или имя входа. В результате наибольшая выгода от их создания заключается в том, чтобы изначально создавать их только на критически важных для бизнеса направлениях вашего сайта, например, при заказе продуктов, процессах регистрации клиентов и платежах.
В следующей части этого руководства мы рассмотрим подкрепление тестов некоторыми данными и проблемы, с которыми это связано.
Ссылка: Учебное пособие: Интеграционное тестирование с Selenium — Часть 1 , Учебное пособие: Интеграционное тестирование с Selenium — Часть 2 от нашего партнера JCG Алекса Коллинза в блоге Алекса Коллинза .