Я люблю работать над клиентскими проектами, потому что они помогают мне по-настоящему понять, как используется Гобелен, и какие проблемы возникают у людей. Обучение на месте является еще одним хорошим способом увидеть, где теория встречается (или не попадает) в реальность.
В любом случае, сейчас я работаю на пару клиентов, для которых тестирование, по праву, очень важно. Мой обычный подход заключается в написании модульных тестов для проверки конкретных случаев ошибок (или других необычных случаев), а затем в написании интеграционных тестов для выполнения основных сценариев использования. Я считаю, что это сбалансированный подход, который признает, что многое из того, что делает Гобелен, — это интеграция.
Одна из причин, по которой мне нравится TestNG, заключается в том, что он плавно охватывает юнит-тесты и интеграционные тесты. Все внутренние тесты Tapestry (около 1500 отдельных тестов) написаны с использованием TestNG, а Tapestry включает базовый класс тестовых примеров для работы с Selenium : AbstractIntegrationTestSuite . Этот класс делает несколько полезных вещей:
- Запускает ваше приложение с помощью Jetty
- Запускает SeleniumServer (который управляет веб-браузером, который может использовать ваше приложение)
- Создает экземпляр клиента Selenium
- Реализует все методы Selenium , перенаправляя каждый на экземпляр Selenium
- Добавляет дополнительные отчеты об ошибках при любых неудачных вызовах клиента Selenium
Это все полезные вещи, но класс немного затормозил … у него есть пара критических недостатков:
- Он запускает ваше приложение с помощью Jetty 5 (в комплекте с SeleniumServer)
- Он запускает и останавливает стек (Selenium, SeleniumServer, Jetty) вокруг каждого класса
Для моего текущего клиента пара ресурсов требует JNDI, и поэтому я использую Jetty 7 для запуска приложения (по крайней мере, в разработке, а возможно и в развертывании). К счастью, Jetty 5 использует старые пакеты org.mortbay.jetty, а Jetty 7 использует новые пакеты org.eclipse.jetty, поэтому обе версии сервера могут сосуществовать в одном приложении.
Большая проблема в том, что я не хотел ни одного титанического теста для всего моего приложения; Сначала я хотел разбить его по-другому, на странице «Гобелен».
Я мог бы создать дополнительные подклассы AbstractIntegrationTestSuite, но тогда тесты потратят огромное количество времени на запуск и остановку Firefox и его друзей. Я действительно хочу, чтобы это началось только один раз .
Я немного рефакторинг, используя некоторые возможности TestNG, которые я ранее не использовал.
Часть AbstractIntegrationTestSuite, отвечающая за запуск и остановку стека , разбита на собственный класс. Этот новый класс, SeleniumLauncher, отвечает за запуск и остановку стека вокруг всего теста TestNG . В терминологии TestNG набор содержит несколько тестов , а тест содержит тестовые случаи (найденные в отдельных классах в отсканированных пакетах). Контрольный пример содержит методы тестирования и настройки.
Вот что я придумала:
package com.myclient.itest; import org.apache.tapestry5.test.ErrorReportingCommandProcessor; import org.eclipse.jetty.server.Server; import org.openqa.selenium.server.RemoteControlConfiguration; import org.openqa.selenium.server.SeleniumServer; import org.testng.ITestContext; import org.testng.annotations.AfterTest; import org.testng.annotations.BeforeTest; import com.myclient.RunJetty; import com.thoughtworks.selenium.CommandProcessor; import com.thoughtworks.selenium.DefaultSelenium; import com.thoughtworks.selenium.HttpCommandProcessor; import com.thoughtworks.selenium.Selenium; public class SeleniumLauncher { public static final String SELENIUM_KEY = "myclient.selenium"; public static final String BASE_URL_KEY = "myclient.base-url"; public static final int JETTY_PORT = 9999; public static final String BROWSER_COMMAND = "*firefox"; private Selenium selenium; private Server jettyServer; private SeleniumServer seleniumServer; /** Starts the SeleniumServer, the application, and the Selenium instance. */ @BeforeTest(alwaysRun = true) public void setup(ITestContext context) throws Exception { jettyServer = RunJetty.start(JETTY_PORT); seleniumServer = new SeleniumServer(); seleniumServer.start(); String baseURL = String.format("http://localhost:%d/", JETTY_PORT); CommandProcessor cp = new HttpCommandProcessor("localhost", RemoteControlConfiguration.DEFAULT_PORT, BROWSER_COMMAND, baseURL); selenium = new DefaultSelenium(new ErrorReportingCommandProcessor(cp)); selenium.start(); context.setAttribute(SELENIUM_KEY, selenium); context.setAttribute(BASE_URL_KEY, baseURL); } /** Shuts everything down. */ @AfterTest(alwaysRun = true) public void cleanup() throws Exception { if (selenium != null) { selenium.stop(); selenium = null; } if (seleniumServer != null) { seleniumServer.stop(); seleniumServer = null; } if (jettyServer != null) { jettyServer.stop(); jettyServer = null; } } }
Обратите внимание, что мы используем аннотации @BeforeTest и @AfterTest; это означает, что любое количество тестов может выполняться с использованием одного и того же стека. Стек запускается только один раз.
Также обратите внимание на то, как мы используем ITestContext для передачи информации тестам в форме атрибутов. TestNG имеет встроенную форму внедрения зависимостей; любой метод, которому нужен ITestContext, может получить его, просто объявив параметр этого типа.
AbstractIntegrationTestSuite2 — это новый базовый класс для написания интеграционных тестов:
package com.myclient.itest; import java.lang.reflect.Method; import org.apache.tapestry5.test.AbstractIntegrationTestSuite; import org.apache.tapestry5.test.RandomDataSource; import org.testng.Assert; import org.testng.ITestContext; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import com.mchange.util.AssertException; import com.thoughtworks.selenium.Selenium; public abstract class AbstractIntegrationTestSuite2 extends Assert implements Selenium { public static final String BROWSERBOT = "selenium.browserbot.getCurrentWindow()"; public static final String SUBMIT = "//input[@type='submit']"; /** * 15 seconds */ public static final String PAGE_LOAD_TIMEOUT = "15000"; private Selenium selenium; private String baseURL; protected String getBaseURL() { return baseURL; } @BeforeClass public void setup(ITestContext context) { selenium = (Selenium) context .getAttribute(SeleniumLauncher.SELENIUM_KEY); baseURL = (String) context.getAttribute(SeleniumLauncher.BASE_URL_KEY); } @AfterClass public void cleanup() { selenium = null; baseURL = null; } @BeforeMethod public void indicateTestMethodName(Method testMethod) { selenium.setContext(String.format("Running %s: %s", testMethod .getDeclaringClass().getSimpleName(), testMethod.getName() .replace("_", " "))); } /* Start of delegate methods */ public void addCustomRequestHeader(String key, String value) { selenium.addCustomRequestHeader(key, value); } ... }
Внутри аннотированного метода @ BeforeClass мы получаем тестовый контекст и извлекаем экземпляр селена и базовый URL-адрес, помещенный туда SeleniumLauncher.
Последним фрагментом головоломки является код, который запускает Jetty. Обычно я тестирую свои веб-приложения с помощью плагина Eclipse run-jetty-run , но RJR не поддерживает функциональность «Jetty Plus», включая JNDI. Таким образом, я создал приложение для запуска Jetty:
package com.myclient; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.WebAppContext; public class RunJetty { public static void main(String[] args) throws Exception { start().join(); } public static Server start() throws Exception { return start(8080); } public static Server start(int port) throws Exception { Server server = new Server(port); WebAppContext webapp = new WebAppContext(); webapp.setContextPath("/"); webapp.setWar("src/main/webapp"); // Note: Need jetty-plus and jetty-jndi on the classpath; otherwise // jetty-web.xml (where datasources are configured) will not be // read. server.setHandler(webapp); server.start(); return server; } }
Это все выглядит отлично. Я ожидаю переместить этот код в Tapestry 5.2 довольно скоро. О чем я озадачиваю, так это о нескольких дополнительных идеях:
- Повышенная гибкость при запуске Jetty, так что вы можете подключить свою собственную конфигурацию сервера Jetty.
- Возможность запуска нескольких агентов браузера, чтобы один набор тестов мог работать с Internet Explorer, Firefox, Safari и т. Д. Во многих случаях один и тот же метод тестирования может вызываться несколько раз для тестирования на разных браузерах.
В любом случае, это всего лишь одна из множества очень крутых идей, которые я ожидаю внедрить в Tapestry 5.2 в ближайшем будущем.