В погоне за качеством кода: Повторяемые системные тесты

Автоматизация управления контейнерами с помощью Cargo

Эндрю Гловер представляет программное обеспечение (ПО) Cargo. Это среда с открытым исходным кодом, которая автоматизирует управление контейнерами, так что можно каждый раз писать логически повторяемые системные тесты.

Эндрю Гловер, президент компании, Stelligent Incorporated

Эндрю Гловер является президентом компании Stelligent Incorporated , которая помогает другим фирмам решать проблемы качества программного обеспечения. Для этого используются эффективные стратегии тестирования и технологии непрерывной интеграции, которые позволяют коллективам разработчиков постоянно контролировать качество кода, начиная с ранних стадий разработки. Просмотрите блог Энди , там можно найти список его публикаций.



12.02.2007

По самой своей сути такие среды тестирования, как JUnit и TestNG, способствуют созданию повторяемых тестов. Поскольку эти среды эффективно используют надежный механизм булевской логики (в форме методов assert), то тестирование можно проводить без вмешательства человека. На самом деле автоматизация является одним из основных преимуществ сред тестирования. Я могу написать довольно сложный тест, исходя из определенного поведения системы, и если это поведение когда-то изменится, то среда выдаст ошибку, которую каждый сможет интерпретировать.

Использование развитой системы тестирования изначально дает преимущество повторяемости среды. Но логическая повторяемость зависит от вас. Например, рассмотрим задачу создания повторяемых тестов, которые проверяют Web-приложения. Некоторые среды, расширяющие функциональность системы JUnit (например, JWebUnit и HttpUnit), выделяются своей поддержкой автоматизированного Web-тестирования. Но сделать структуру теста повторяемой остается задачей разработчика. И когда дело доходит до развертывания ресурсов Web-приложения, это становится трудно осуществимым.

Реальная конструкция теста JWebUnit довольно проста. Она показана в Листинге 1:

Листинг 1. Простой тест JWebUnit
package test.come.acme.widget.Web;

import net.sourceforge.jwebunit.WebTester;
import junit.framework.TestCase;

public class WidgetCreationTest extends TestCase {
 private WebTester tester;

 protected void setUp() throws Exception {
  this.tester = new WebTester();
  this.tester.getTestContext().
   setBaseUrl("http://localhost:8080/widget/");	
 }

 public void testWidgetCreation() {
  this.tester.beginAt("/CreateWidget.html");		
  this.tester.setFormElement("widget-id", "893-44");
  this.tester.setFormElement("part-num", "rt45-3");

  this.tester.submit();		
  this.tester.assertTextPresent("893-44");
  this.tester.assertTextPresent("successfully created.");
 }
}

Этот тест взаимодействует с Web-приложением и пытается создать на основе этого взаимодействия стандартный элемент графического интерфейса пользователя (GUI -- graphical user interface). Затем тест проверяет, что этот элемент был успешно создан. Однако читатели предыдущих статей этой серии могут заметить едва уловимую проблему с повторяемостью теста. А вы ее видите? Как вы думаете, что произойдет, если этот контрольный пример проделать последовательно дважды?

Повысьте качество своего кода

Не забудьте посетить сопутствующий дискуссионный форум Эндрю. Там можно получить помощь по метрикам кода, средам тестирования и написанию качественного кода.

Судя по идентификационному аспекту экземпляра стандартного объекта GUI (то есть widget-id), можно наверняка предположить, что ограничения базы данных (БД) в приложении запрещают создание дополнительного объекта такого рода, который уже существует До запуска другого теста необходим какой-то процесс для удаления стандартного объекта GUI, предназначенного для контрольного примера. Иначе существует высокая вероятность того, что при запуске два раза подряд контрольный пример завершится ошибкой.

К счастью, как я уже писал в предыдущей статье, существует механизм, который обеспечивает повторяемость БД-зависимых контрольных примеров, а именно DbUnit.

DbUnit выходит на сцену

Улучшить контрольный пример из Листинга 1, чтобы задействовать DbUnit, довольно просто. Все, что нужно для DbUnit, -- это вставить некоторые данные в БД и соответствующее к ней подключение. Это показано в Листинге 2:

Листинг 2. БД-зависимое тестирование с помощью DbUnit
package test.come.acme.widget.Web;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;

import net.sourceforge.jwebunit.WebTester;
import junit.framework.TestCase;

public class RepeatableWidgetCreationTest extends TestCase {
 private WebTester tester;

 protected void setUp() throws Exception {
  this.handleSetUpOperation();
  this.tester = new WebTester();
  this.tester.getTestContext().
   setBaseUrl("http://localhost:8080/widget/");	
 }

 public void testWidgetCreation() {
  this.tester.beginAt("/CreateWord.html");		
  this.tester.setFormElement("widget-id", "893-44");
  this.tester.setFormElement("part-num", "rt45-3");

  this.tester.submit();		
  this.tester.assertTextPresent("893-44");
  this.tester.assertTextPresent("successfully created.");
 }

 private void handleSetUpOperation() throws Exception{
  final IDatabaseConnection conn = this.getConnection();
  final IDataSet data = this.getDataSet();        
  try{
   DatabaseOperation.CLEAN_INSERT.execute(conn, data);
  }finally{
   conn.close();
  }
 }

 private IDataSet getDataSet() throws IOException, DataSetException {
  return new FlatXmlDataSet(new File("test/conf/seed.xml"));
 }

 private IDatabaseConnection getConnection() throws 
   ClassNotFoundException, SQLException {
    Class.forName("org.hsqldb.jdbcDriver");
    final Connection jdbcConnection = 
     DriverManager.getConnection("jdbc:hsqldb:hsql://127.0.0.1", 
	  "sa", "");
    return new DatabaseConnection(jdbcConnection);
 }
}

Благодаря добавлению DbUnit, контрольный пример стал по-настоящему повторяемым. Каждый раз при запуске контрольного примера в методе handleSetUpOperation() DbUnit выполняет над данными операцию CLEAN_INSERT. По существу, эта операция очищает БД от старых данных и вставляет новые, удаляя таким образом все созданные до этого стандартные объекты GUI.

Еще раз: что же такое DbUnit?

DbUnit -- это расширение JUnit, которое обеспечивает перевод БД в известное состояние между запусками тестов. Разработчики используют seed-файлы XML для вставки в БД особых данных, на которые может опираться контрольный пример. Таким образом, DbUnit обеспечивает повторяемость контрольных примеров, которые зависят от одной или более БД.

Но это не означает, что я закрываю тему повторяемости контрольных примеров. На самом деле, я только начал.

Повторение системных тестов

Контрольные примеры, определенные в Листинге 1 и Листинге 2, относятся к тем тестам, которые я предпочитаю называть системными тестами. Поскольку системные тесты имеют дело с полностью установленным приложением, например, Web-приложением, они обычно содержат в себе контейнер сервлета и связанную БД. Эти тесты направлены на проверку того, насколько работа внешних интерфейсов (например, в случае Web-приложения это будут Web-страницы) соответствует проекту.

Гибкость приоритетов

Практический совет: по возможности избегайте наследования контрольных примеров. Многие среды тестирования, являющиеся расширением JUnit, предлагают специализированные контрольные примеры, от которых можно организовать наследование для организации тестирования определенной архитектуры. Однако контрольные примеры, которые наследуют классы от такой среды, страдают от недостатка гибкости. Это происходит из-за парадигмы™ прямого наследования, принятой для Java-платформы. Часто те же самые расширения JUnit предлагают API делегирования, что облегчает комбинирование различных сред без использования строгой структуры наследования.

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

  1. Создание war-файла, содержащего все связанные с тестом Web-артефакты, то есть JSP-файлы, сервлеты, jar-файлы сторонних производителей, изображения и т.д.;
  2. Размещение war-файла в целевом Web-контейнере. (Если контейнер не запущен, его надо запустить.);
  3. Запуск всех соответствующих БД. (Если БД нуждается в обновлении схемы, ее надо обновить до запуска.)

Сколько работы ради одного жалкого теста! Как вы думаете, если этот процесс требует столько времени, как часто будет запускаться тест? Если системные тесты необходимо сделать логически повторяемыми (скажем, в среде непрерывной интеграции), этот список шагов определенно неприемлем.


Введение в Cargo

Хорошая новость: все основные шаги из предыдущего списка можно автоматизировать. На самом деле, если вы занимаетесь Web-разработкой на Java, то наверняка уже автоматизировали первый шаг с помощью Ant, Maven или какого-то другого средства сборки.

Однако второй шаг представляет собой интересную проблему. Автоматизация Web-контейнера может быть непростой задачей. Например, некоторые контейнеры обладают особыми задачами в Ant, которые обеспечивают их автоматическое размещение и запуск, но эти задачи для каждого контейнера являются особыми. Более того, эти задачи исходят из некоторых предположений. Например, где установлен контейнер и, что более важно, что контейнер уже установлен.

Cargo -- это инновационный проект с открытым исходным кодом. Он имеет своей целью автоматизацию управления контейнерами на базе общей технологии, чтобы одинаковый интерфейс API использовался для развертывания WAR-файла на JBoss и для запуска и остановки Jetty. Cargo также может автоматически загружать и устанавливать контейнер. Cargo API можно использовать по-разному, начиная с Java-кода для задач в Ant и заканчивая целями Maven.

Использование инструмента, подобного Cargo, решает одну из главных задач по написанию логически повторяемых контрольных примеров. К тому же можно создать сборку, которая задействует возможности Cargo для автоматического осуществления следующих задач:

  1. Загрузка нужного контейнера;
  2. Установка контейнера;
  3. Запуск контейнера;
  4. Размещение выбранного WAR- или EAR-файла в контейнере.

Довольно удобно, а? И, наконец, Cargo можно использовать для останова выбранного контейнера.

"Язык" Cargo

Неплохой идеей будет изучить основы Cargo еще до того, как окунуться туда с головой. А именно хочется быть уверенным в том, что вы понимаете концепцию контейнеров и управления ими с точки зрения Cargo.

Для начинающих, очевидно, необходимо познакомиться с самим понятием контейнера. Контейнеры -- это сервлеты, которые служат сервером для приложений. Это могут быть Web-приложения, приложения на базе EJB, или приложения сразу обоих типов. Поэтому существуют Web-контейнеры и EJB-контейнеры. Tomcat -- это Web-контейнер, а JBoss можно рассматривать как EJB-контейнер. Поэтому Cargo поддерживает не так уж и много контейнеров. Для своих примеров я буду использовать Tomcat версии 5.0.28. (В Cargo это называлось бы контейнер "tomcat5x".)

Далее, если контейнер еще не установлен, можно использовать Cargo для загрузки и установки конкретного контейнера. Для этого нужно указать в Cargo URL для загрузки. После установки контейнера Cargo также позволяет использовать для его настройки опции конфигурации. Эти опции принимают форму пары "имя-значение".

И наконец, есть понятие размещаемого ресурса, который в моем примере представляет собой WAR-файл. Заметьте, что это точно также мог бы быть и EAR-файл.

Запомните эти концепции. А теперь давайте посмотрим, что мы можем сделать с помощью Cargo.


Cargo в действии

В пример для этой статьи входит использование Cargo в Ant, что влечет за собой упаковку системного теста, который я определил ранее, с помощью задач Cargo Ant. Эти задачи затем устанавливают, запускают, размещают и останавливают контейнер. Мы сначала сделаем установку, запустим тест, а затем остановим контейнер.

Первый шаг, который необходимо выполнить для использования Cargo в сборке Ant, -- это предоставление описания для всех задач Cargo. Это позволяет позже ссылаться на задачи Cargo в файле сборки. Для осуществления этого этапа есть множество способов. Листинг 3 просто загружает задачи из файла свойств, который находится в JAR-файлах Cargo:

Листинг 3. Загрузка всех задач Cargo в Ant
<taskdef resource="cargo.tasks">
 <classpath>
  <pathelement location="${libdir}/${cargo-jar}"/>
  <pathelement location="${libdir}/${cargo-ant-jar}"/>
 </classpath>
</taskdef>

После определения задач Cargo и начинается настоящая работа. Листинг 4 описывает задачи Cargo по загрузке, установке и запуску контейнера Tomcat . Задача zipurlinstaller загружает и устанавливает Tomcat из http://www.apache.org/dist/tomcat/tomcat-5/v5.0.28/bin/ jakarta-tomcat-5.0.28.zip в локальный временный каталог.

Листинг 4. Загрузка и установка Tomcat 5.0.28
<cargo containerId="tomcat5x" action="start" 
       wait="false" id="${tomcat-refid}">
	   
 <zipurlinstaller installurl="${tomcat-installer-url}"/>
 
 <configuration type="standalone" home="${tomcatdir}">
  <property name="cargo.remote.username" value="admin"/>
  <property name="cargo.remote.password" value=""/>

  <deployable type="war" file="${wardir}/${warfile}"/>

 </configuration>		
 
</cargo>

Заметьте, что для запуска и останова контейнера из различных задач, как бы вам хотелось сделать, необходимо связать контейнер с уникальным идентификатором, который является аспектом id="${tomcat-refid}" задачи cargo.

Также заметим, что конфигурационные аспекты Tomcat решаются внутри задачи cargo. В случае Tomcat необходимо установить свойства username (имя пользователя) и password (пароль). И, наконец, определяем указатель на WAR-файл с помощью элемента deployable .

Свойства Cargo

Все свойства, используемые в задачах Cargo, показаны в Листинге 5. Например, tomcatdir определяет одно или два местоположения, где будет установлен Tomcat. Это конкретное место представляет собой зеркально отображенную структуру, на которую можно ссылаться из реального загруженного и установленного экземпляра Tomcat (который находится во временном каталоге). Свойство tomcat-refid также помогает ассоциировать уникальный экземпляр контейнера.

Листинг 5. Свойства Cargo
<property name="tomcat-installer-url" 
  value="http://www.apache.org/dist/tomcat/tomcat-5/v5.0.28/bin/
    jakarta-tomcat-5.0.28.zip"/>
<property name="tomcatdir" value="target/tomcat"/>	
<property name="tomcat.username" value="admin"/>	
<property name="tomcat.passwrd" value=""/>	
<property name="wardir" value="target/war"/>
<property name="warfile" value="words.war"/>	
<property name="tomcat-refid" value="tmptmct01"/>

Для останова контейнера можно определить задачу, которая ссылается на свойство tomcat-refid, как показано в Листинге 6:

Листинг 6. Останов контейнера в стиле Cargo
<cargo containerId="tomcat5x" action="stop" 
       refid="${tomcat-refid}"/>

Упаковка в Cargo

Листинг 7 объединяет код Листинге 4 и Листинге 6. В нем тестовое задание упаковывается с помощью двух задач Cargo: одной для запуска и одной -- для остановки Tomcat. Задача antcall вызывает цель под названием _run-system-tests, которая описана в Листинге 8.

Листинг 7. Упаковка тестового задания с помощью Cargo
<target name="system-test" if="Junit.present" 
        depends="init,junit-present,compile-tests,war">

 <cargo containerId="tomcat5x" action="start" 
       wait="false" id="${tomcat-refid}">			
  <zipurlinstaller installurl="${tomcat-installer-url}"/>
  <configuration type="standalone" home="${tomcatdir}">
   <property name="cargo.remote.username" value="admin"/>
   <property name="cargo.remote.password" value=""/>
   <deployable type="war" file="${wardir}/${warfile}"/>
  </configuration>
 </cargo>

 <antcall target="_run-system-tests"/>

 <cargo containerId="tomcat5x" action="stop" 
       refid="${tomcat-refid}"/>			

</target>

Листинг 8 определяет тестовое задание под названием _run-system-tests. Заметим, что эта задача лишь запускает системные тесты, которые размещены в каталоге test/system. Например, в этом каталоге размещен контрольный пример, определенный в Листинге 2.

Листинг 8. Запуск JUnit через Ant
<target name="_run-system-tests">
 <mkdir dir="${testreportdir}"/>   
 <junit dir="./" failureproperty="test.failure" 
        printSummary="yes" fork="true" 
		haltonerror="true">
  <sysproperty key="basedir" value="."/>     
  <formatter type="xml"/>      
  <formatter usefile="false" type="plain"/>     
  <classpath>
   <path refid="build.classpath"/>       
   <pathelement path="${testclassesdir}"/>
   <pathelement path="${classesdir}"/>      
  </classpath>
  <batchtest todir="${testreportdir}">
   <fileset dir="test/system">
    <include name="**/**Test.java"/> 
   </fileset>
  </batchtest>
 </junit>
</target>

В Листинге 7файл сборки Ant был полностью сконфигурирован для упаковки системных тестов с помощью технологии развертывания Cargo. Код в Листинге 7 обеспечивает логическую повторяемость всех системных тестов из Листинга 8 в каталоге test/system . Эти системные тесты можно в любой момент запустить на любой машине, что идеально подходит для среды непрерывной интеграции. Эти тесты не делают никаких предположений о контейнере, причем не только о его местоположении, но и даже о том, запущен ли он! (Конечно, эти тесты по-прежнему исходят из одного предположения, которое я не упомянул. А именно из того, что используемая БД правильно настроена и работает. Но это предмет отдельного разговора.)


Повторяемые результаты

В Листинге 9 вы можете посмотреть результаты своих усилий. При выдаче сборке Ant команды system-test запускаются системные тесты. Cargo берет на себя все детали управления выбранным контейнером, не делая никаких нарушающих повторяемость предположений относительно среды тестирования.

Листинг 9. Улучшенная сборка
war:
 [war] Building war: C:\dev\projects\acme\target\widget.war

system-test:

_run-system-tests:
 [mkdir] Created dir: C:\dev\projects\acme\target\test-reports
 [junit] Running test.come.acme.widget.Web.RepeatableWordCreationTest
 [junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 4.53 sec
 [junit] Testcase: testWordCreation took 4.436 sec

BUILD SUCCESSFUL
Total time: 1 minute 2 seconds

Учтите, что Cargo также работает и в сборках Maven. Более того, Cargo Java API обеспечивает программное управление контейнерами везде, от обычных приложений и до контрольных примеров. И ПО Cargo предназначено не только для JUnit (хотя примеры кода и написаны в JUnit). Пользователи TestNG будут счастливы узнать, что Cargo работает также и для их тестовых пакетов. На самом деле не имеет значения, на чем написаны тесты. Просто упакуйте их с помощью Cargo - и все вопросы управления контейнерами будут идеально автоматизированы!


Заключение

Обеспечивать логическую повторяемость своих тестов можете лишь вы сами, но Cargo может оказать в этом действеную помощь. Cargo управляет средой контейнера, так что вам не надо об этом заботиться. Включите Cargo в свою программу тестирования, и это ПО определенно сократит работу по созданию повторяемых тестов, которые проверяют Web-приложения.

Ресурсы

Научиться

Получить продукты и технологии

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=195373
ArticleTitle=В погоне за качеством кода: Повторяемые системные тесты
publish-date=02122007