На прошлой неделе я писал о подходе, который мы используем в XebiaLabs для тестирования интеграции с различными продуктами промежуточного программного обеспечения, которые поддерживает наш продукт автоматизации развертывания Java EE, который поддерживает Deployit .
Блог закончился обещанием, что я буду обсуждать, как проверить, действительно ли приложение может использовать конфигурации, которые интегрируются в наше промежуточное ПО (иначе говоря, шаги) Создайте. Но прежде чем углубляться в это, давайте сначала ответим на вопрос, зачем нам это нужно. Если код может настраивать источник данных без сервера приложений, он должен быть в порядке, чтобы приложение использовало его, верно? Ну не всегда. Хотя WebSphere и WebLogic содержат некоторые функции для проверки соединения с базой данных и, таким образом, проверки правильности настройки источника данных, эта функциональность недоступна для других конфигураций, таких как параметры JMS. А у JBoss такой функциональности нет вообще. Поэтому возникает вопрос: как мы можем доказать, что приложение действительно может работать с конфигурациями, созданными нашими шагами?
Введите большое старое тестовое приложение
Первым делом мы попытались написать тестовое приложение, которое бы выполняло все функции сервера приложений Java EE. Что-то вроде приложения Java EE Pet Store . Итак, мы запустили Eclipse WTP и написали несколько сервлетов / JSP / контроллеров Spring, которые выполняют такие функции, как запрос к таблице, размещение сообщения в очереди и получение и так далее. Мы упаковали это приложение в EAR, развернули его на нашем целевом сервере приложений и проверили, правильно ли оно.
Мы могли бы сделать это частью нашей непрерывной интеграции, написав тест JUnit, который развертывает это приложение на каждом из серверов приложений, а затем выполняет приложение, запрашивая разные URL-адреса. Но есть несколько проблем с этим решением:
- Каждый раз, когда вы изменяете тестовое приложение, вы должны перестраивать его и помещать полученный EAR-файл в каталог тестовых ресурсов (src / test / resources / …) для проекта внедрения промежуточного программного обеспечения.
- Существует только одно тестовое приложение для всех трех поддерживаемых серверов приложений: IBM WebSphere Application Server, Oracle WebLogic Server и JBoss Application Server. Но тесты, которые вы могли бы сделать для каждого, немного отличаются. Например, WebSphere предлагает очереди для встроенной шины интеграции служб , для WebSphere MQ и для обычных поставщиков JMS. Можно было бы сделать три отдельных приложения для испытаний, но это только усложняет ситуацию.
Основная проблема здесь заключается в том, что это тестовое приложение и другая часть теста (часть, описанная в части 1 этой серии) хранятся в разных местах, как это. Разве вещи не сработали бы намного лучше, если бы эти кусочки кода были вместе?
На самом деле, не было бы неплохо, если бы мы могли создавать эти тестовые приложения на лету? С помощью всего лишь небольшого количества тестового кода, который мы хотим запустить на сервере приложений в контексте Java EE? Да? Ну, я надеялся, что вы согласитесь, потому что это именно то, что делает библиотека OnTheFly, которую мы написали.
Создание JAR-файлов на лету
Первое, что нам нужно сделать, это убедиться, что мы можем создать файл JAR на лету. Если у нас это есть, мы можем создавать файлы WAR и EAR. Это код для нашего класса JarOnTheFly:
import java.io.*;
import java.util.*;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.Resource;
public class JarOnTheFly {
private Map<String, Resource> files = new HashMap<String, Resource>();
public void addFile(String filename, Resource resource) {
files.put(filename, resource);
}
public void write(File jarFile) throws IOException {
FileOutputStream jarFileOut = new FileOutputStream(jarFile);
try {
JarOutputStream jarOut = new JarOutputStream(jarFileOut);
try {
for (Map.Entry<String, Resource> eachFile : files.entrySet()) {
String filename = eachFile.getKey();
Resource resource = eachFile.getValue();
jarOut.putNextEntry(new JarEntry(filename));
InputStream resourceIn = resource.getInputStream();
try {
IOUtils.copy(resourceIn, jarOut);
} finally {
IOUtils.closeQuietly(resourceIn);
}
jarOut.closeEntry();
}
} finally {
IOUtils.closeQuietly(jarOut);
}
} finally {
IOUtils.closeQuietly(jarFileOut);
}
}
protected File writeToTemporaryFile(String prefix, String suffix) throws IOException {
File tempJarFile = File.createTempFile(prefix, suffix);
write(tempJarFile);
return tempJarFile;
}
}
Вы можете создать экземпляр этого класса, добавить в него файлы с помощью метода addFile, а затем записать его в определенный файл или во временный файл. Поскольку вы можете передать любой класс, который реализует интерфейс Spring Resource, вы можете поместить любой тип содержимого в файл JAR: обычные файлы , файлы из classpath , байтовые массивы в памяти и даже входные потоки .
Создание файлов WAR на лету
Класс WarOnTheFly основан на классе JarOnTheFly, чтобы добавить ряд специфических функций WAR. Прежде всего, можно создать минимальный WEB-INF / web.xml. Если вы вызываете метод writeToTeoraryFile, это будет сделано автоматически, и расширение автоматически установится на .war.
Во-вторых, и самое главное, вы можете добавить любой класс из вашего текущего пути к классу в файл WAR в качестве сервлета. Название просто класс добавляется в WEB-INF / web.xml как имя сервлета.
Хотя к этому WAR-файлу также можно добавить тестовые JSP, возможность написать свой тестовый код в виде сервлета — вот что делает этот подход таким приятным. Код тестового сервлета может быть расположен рядом с остальным тестовым кодом, чтобы он всегда был перед вами, пока вы работаете над тестами. Ограничением текущей реализации является то, что ваш тестовый сервлет должен содержаться ровно в одном тестовом классе; любые вспомогательные классы или внутренние классы не будут скопированы в файл WAR.
import java.io.*;
import java.util.*;
import org.apache.velocity.VelocityContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
public class WarOnTheFly extends JarOnTheFly {
private String name;
private Map<String, String> servlets;
public WarOnTheFly(String name) {
this.name = name;
this.servlets = new HashMap<String, String>();
}
public void addServlet(Class<? extends Servlet> servletClass) {
String servletName = servletClass.getSimpleName();
String servletClassFilename = servletClass.getName().replace('.', '/') + ".class";
addFile("WEB-INF/classes/" + servletClassFilename,
new ClassPathResource(servletClassFilename));
servlets.put(servletName, servletClass.getName());
}
public void addWebXml() {
VelocityContext context = new VelocityContext();
context.put("name", name);
context.put("servlets", servlets);
String webxml = evaluateTemplate(context,
"com/xebialabs/deployit/test/support/onthefly/web.xml.vm");
addFile("WEB-INF/web.xml", new ByteArrayResource(webxml.getBytes()));
}
public File writeToTemporaryFile() throws IOException {
addWebXml();
return writeToTemporaryFile(name, ".war");
}
public String getName() {
return name;
}
}
МетодvaluTemplate, который вызывается из метода addWebXml, представляет собой метод статической утилиты, который считывает шаблон Velocity из указанного местоположения classpath и оценивает его, используя указанный контекст. Файл web.xml.vm выглядит следующим образом:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4"
xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<display-name>${name}</display-name>
#foreach( $key in $servlets.keySet() )
<servlet>
<servlet-name>$key</servlet-name>
<servlet-class>$!servlets.get($key)</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>$key</servlet-name>
<url-pattern>/$key</url-pattern>
</servlet-mapping>
#end
</web-app>
Создание EAR-файлов на лету
Чтобы не быть укушенным тем фактом, что каждый сервер приложений требует от вас указания корневого контекста для файла WAR по-своему, нам нужно обернуть файл WAR в файл EAR. Это то, что делает класс EarOnTheFly. Реализация не должна стать большим сюрпризом после того, как вы увидели классы JarOnTheFly и WarOnTheFly:
import java.io.*;
import java.util.*;
import org.apache.velocity.VelocityContext;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
public class EarOnTheFly extends JarOnTheFly {
private String name;
private Map<String, String> wars;
public EarOnTheFly(String name) {
this.name = name;
this.wars = new HashMap<String, String>();
}
public void addWarOnTheFly(WarOnTheFly wotf) throws IOException {
String warFilename = wotf.getName() + ".war";
Resource warFile = new FileSystemResource(wotf.writeToTemporaryFile());
addFile(warFilename, warFile);
wars.put(wotf.getName(), warFilename);
}
private void addApplicationXml() {
VelocityContext context = new VelocityContext();
context.put("name", name);
context.put("wars", wars);
String webxml = evaluateTemplate(context,
"com/xebialabs/deployit/test/support/onthefly/application.xml.vm");
addFile("META-INF/application.xml", new ByteArrayResource(webxml.getBytes()));
}
public File writeToTemporaryFile() throws IOException {
addApplicationXml();
return writeToTemporaryFile("itest", ".ear");
}
public String getName() {
return name;
}
}
Для полноты вот содержимое шаблона application.xml.vm Velocity:
<?xml version="1.0" encoding="UTF-8"?> <application id="Application_ID" version="1.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/application_1_4.xsd"> <display-name>${name}</display-name> <module> #foreach( $key in $wars.keySet() ) <web> <web-uri>$!wars.get($key)</web-uri> <context-root>$key</context-root> </web> #end </module> </application>
Тестовый сервлет
Итак, как мы используем эти классы? Мы начнем с написания нашего тестового сервлета. При вызове тестовый сервлет должен выполнить тестируемую конфигурацию Java EE и, наконец, распечатать конкретное сообщение, когда тест пройден успешно. Если что-то идет не так, может быть напечатано сообщение об ошибке или может быть выдано исключение. Следующий код — это сервлет, который мы используем для проверки шагов нашего источника данных:
public class DataSourceStepsItestServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException
{
try {
InitialContext ic = new InitialContext();
DataSource ds = (DataSource) ic.lookup("jdbc/itest");
Connection conn = ds.getConnection();
try {
Statement stmt = conn.createStatement();
try {
stmt.executeUpdate("CREATE TABLE TEST_TABLE " +
"(THEVALUE VARCHAR(16) )");
} catch (SQLException ignore) {
}
String expectedValue = Integer.toString(new Random().nextInt());
stmt.executeUpdate("INSERT INTO TEST_TABLE (THEVALUE) " +
"VALUES ( \'" + expectedValue + "\' ) ");
String sql = "SELECT * FROM TEST_TABLE";
boolean found = false;
ResultSet rs = stmt.executeQuery(sql);
try {
while (rs.next()) {
String returnValue = rs.getString(1);
if (!found && returnValue.equals(expectedValue)) {
found = true;
}
}
} finally {
rs.close();
}
if (!found) {
throw new RuntimeException("INSERTED \"" + expectedValue +
" into table TEST_TABLE but could not get it back");
}
resp.setContentType("text/plain");
PrintWriter out = resp.getWriter();
out.println("The middleware integration test has passed!");
} finally {
conn.close();
}
} catch (NamingException exc) {
throw new ServletException(exc);
} catch (SQLException exc) {
throw new ServletException(exc);
}
}
}
Сначала создается тестовая таблица, затем в нее записывается случайная строка и, наконец, содержимое таблицы читается, чтобы увидеть, содержит ли таблица эту случайную строку. Если это сработает, мы можем быть уверены, что установка JDBC верна!
Развертывание тестового сервлета
Следующим шагом является развертывание тестового сервлета на сервере приложений. Таким образом, мы обертываем его в файл WAR, который упаковывается в файл EAR (см. Код ниже), а затем мы используем некоторый специальный код сервера приложений для развертывания этого файла EAR (не показан).
public static EarOnTheFly createItestEarOnTheFly(Class<? extends Servlet> itestServletClass)
throws IOException
{
WarOnTheFly wotf = new WarOnTheFly("itest");
wotf.addServlet(itestServletClass);
EarOnTheFly eotf = new EarOnTheFly("itest");
eotf.addWarOnTheFly(wotf);
}
Запуск тестового сервлета
Теперь, когда наш тестовый сервлет развернут на сервере приложений, мы должны вызвать его и убедиться, что он работает успешно. Для этого мы используем инфраструктуру HttpUnit, поскольку она предоставляет удобный способ вызывать URL-адреса и проверять ответ, который возвращается.
public static void assertItestServlet(Host serverHost, int port,
Class<? extends Servlet> itestServletClass) throws IOException, SAXException
{
String url = "http://" + serverHost.getAddress() + ":" + port + "/" +
ITEST_WAR_NAME + "/" + itestServletClass.getSimpleName();
WebConversation wc = new WebConversation();
WebRequest req = new GetMethodWebRequest(url);
WebResponse resp = wc.getResponse(req);
String responseText = resp.getText();
assertTrue(responseText.contains("The middleware integration test has passed!"));
}
Собираем все вместе
Наконец, мы должны добавить эти тесты на стороне сервера приложений к тестам интеграции промежуточного программного обеспечения, как я описал их в предыдущем блоге. Точные данные зависят от тестируемого сервера приложений, но такой метод тестирования должен выглядеть примерно так из плагина JBoss Deployit:
@Test
public void testDeploymentOfOneDataSourceToExistingServer() throws IOException, SAXException {
dataSource = new JbossasDataSource();
dataSource.setJndiName("jdbc/itest");
dataSource.setDriverClass("org.hsqldb.JdbcDriver");
dataSource.setConnectionUrl("jdbc:hsqldb:file:/tmp/itest.hdb");
dataSource.setUsername("sa");
dataSource.setPassword("");
mbeanName = "jboss.jca:name=" + dataSource.getJndiName() + ",service=ManagedConnectionPool";
assertMBeanDoesNotExist();
createDataSource();
deployItestEar(existingJBossServer, createItestEarOnTheFly(DataSourceStepsItestServlet.class));
restartServer();
assertMBeanExists();
assertItestServlet(existingJBossHost, 8080, DataSourceStepsItestServlet.class);
destroyDataSource();
undeployItestEar();
restartServer();
assertMBeanDoesNotExist();
}
Продолжение следует…
Мы уже давно используем этот подход для тестирования конфигурации промежуточного программного обеспечения, и это дает нам большую уверенность в нашем коде интеграции промежуточного программного обеспечения. Есть только одна часть, о которой я еще не говорил; как интегрировать этот подход с вашей непрерывной интеграцией с использованием Maven и VMware. Подробнее об этом на следующей неделе …