Я люблю работать над клиентскими проектами, потому что они помогают мне по-настоящему понять, как используется Гобелен, и какие проблемы возникают у людей. Обучение на месте является еще одним хорошим способом увидеть, где теория встречается (или не попадает) в реальность.
В любом случае, сейчас я работаю на пару клиентов, для которых тестирование, по праву, очень важно. Мой обычный подход заключается в написании модульных тестов для проверки конкретных случаев ошибок (или других необычных случаев), а затем в написании интеграционных тестов для выполнения основных сценариев использования. Я считаю, что это сбалансированный подход, который признает, что многое из того, что делает Гобелен, — это интеграция.
Одна из причин, по которой мне нравится 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 в ближайшем будущем.