В погоне за качеством кода: Откройте XMLUnit (исходники)

Эндрю Гловер

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

В этом месяце сначала будет показано, почему для проверки структуры и содержимого XML-документов не следует использовать сравнения String. Затем будет представлена среда XMLUnit - инструмент XML-проверки, созданный специально для разработчиков Java, и будет показано, как его использовать для проверки XML-документов.

Старое доброе сравнение строк

Для начала представим, что создано приложение, выводящее XML-документ, представляющий объектно-зависимый отчет. Отчет создается для данного набора классов и соответствующих фильтров и выводит класс и его зависимости (следует подумать об импорте).

В листинге 1 показан отчет для данного списка классов com.acme.web.Widget и com.acme.web.Account с фильтрами, игнорирующими внешние классы, например, java.lang.String:

Листинг 1. Пример зависимости XML-отчета

<DependencyReport date="Sun Dec 03 22:30:21 EST 2006">
  <FiltersApplied>
    <Filter pattern="java/org"/>
    <Filter pattern="net."/>
  </FiltersApplied>
  <Class name="com.acme.web.Widget">
    <Dependency name="com.acme.resource.Configuration"/>
    <Dependency name="com.acme.xml.Document"/>
  </Class>
  <Class name="com.acme.web.Account">
    <Dependency name="com.acme.resource.Configuration"/>
    <Dependency name="com.acme.xml.Document"/>
  </Class>
</DependencyReport>

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

  • Структура;
  • Содержимое;
  • Конкретное содержимое.

Первые два аспекта можно обработать с помощью JUnit и использования сравнений String (см. листинг 2):

Листинг 2. Проверка XML с помощью жеского кодирования

public class XMLReportTest extends TestCase {

 private Filter[] getFilters(){
  Filter[] fltrs = new Filter[2];
  fltrs[0] = new RegexPackageFilter("java/org");
  fltrs[1] = new SimplePackageFilter("net.");
  return fltrs;
 }

 private Dependency[] getDependencies(){
  Dependency[] deps = new Dependency[2];
  deps[0] = new Dependency("com.acme.resource.Configuration");
  deps[1] = new Dependency("com.acme.xml.Document");
  return deps;
 }

 public void testToXML() {
  Date now = new Date();
  BatchDependencyXMLReport report = 
   new BatchDependencyXMLReport(now, this.getFilters());

  report.addTargetAndDependencies(
    "com.acme.web.Widget", this.getDependencies());
  report.addTargetAndDependencies(
    "com.acme.web.Account", this.getDependencies());

  String valid = "<DependencyReport date=\"" + now.toString() + "\">"+
    "<FiltersApplied><Filter pattern=\"java/org\" /><Filter pattern=\"net.\" />"+
    "</FiltersApplied><Class name=\"com.acme.web.Widget\">" +
    " <Dependency name=\"com.acme.resource.Configuration\" />"+
    "<Dependency name=\"com.acme.xml.Document\" /></Class>"+
    "<Class name=\"com.acme.web.Account\">"+
    "<Dependency name=\"com.acme.resource.Configuration\" />"+
    "<Dependency name=\"com.acme.xml.Document\" />"+
    "</Class></DependencyReport>";

   assertEquals("report didn't match xml", valid, report.toXML());
 }
}

Тестирование, представленное в листинге 2, имеет несколько основных недостатков, среди которых не только жестко закодированные сравнения String. Во-первых, тестирование не является четко написанным. Во-вторых, оно крайне неустойчиво. При изменении формата XML-документа (включая добавления пробела), проще вставить новую копию документа, чем пытаться исправить код String. Наконец, сущность тестирования вынуждает разработчиков бороться с аспектом Date, даже если этот аспект ни на что не влияет.

А если требуется убедиться, что значение name второго элемента Class в документе - com.acme.web.Account? Разумеется, можно использовать регулярные выражения или поиск String, но это процесс довольно трудоемкий. Не лучше ли управлять DOM непосредственно с помощью анализирующей среды?

 
XMLUnit с TestNG?

XMLUnit представляет собой расширение JUnit, но это не значит, что его нельзя использовать в TestNG. В TestNG можно встроить практически всю среду при условии, что в ней имеется API, поддерживающий передачу полномочий, и не основанный на декораторе.

Тестирование с помощью XMLUnit

Если появляется ощущение, что работы стало слишком много, можно предположить, что кто-то другой нашел более простой способ решения проблемы. Когда речь идет о программной проверке XML-документов, первое, что приходит на ум, это XMLUnit.

XMLUnit представляет собой расширенную среду JUnit, упрощающую для разработчиков процесс тестирования XML-документов. Фактически XMLUnit можно назвать настоящим хет-триком для XML-тестирования: эту среду можно использовать для проверки структуры XML-документа, его содержимого и даже для проверки определенных фрагментов документов.

Самый простой способ заключается в использовании XMLUnit для логического сравнения XML-документов со стандартными контрольными файлами. По существу, это тестирование на различия : Имея правильный XML-документ, можно сравнить, создает ли приложение такой же документ? Это относительно простой тест, но его можно использовать для проверки структуры и содержимого XML-документа. С помощью XPath можно также проверить определенное содержимое.

 
Делегирование, а не наследование!

В качестве основного правила избегайте наследования тестирования, где только возможно. Многие расширенные среды JUnit, включая XMLUnit, предлагают специализированные тесты, которые могут наследоваться для упрощения тестирования определенной архитектуры. Тестирования, наследующие классы из среды, страдают от отсутствия гибкости из-за иерархической парадигмы платформы Java с прямым наследованием. Более часто такие расширенные среды JUnit предлагают делегирование API, что упрощает комбинирование различных сред без проблем с жесткой структурой наследования.

Проверка содержимого

XMLUnit можно использовать посредством делегирования или наследования. В качестве основного правила я рекомендую избегать наследования при тестировании. С другой стороны, наследование из XMLTestCase среды XMLUnit предоставляет некоторые удобные методы контроля (отличные от static и, следовательно, на них нельзя ссылаться статически, как в TestCase JUnit).

Независимо от выбора режима использования XMLUnit, необходимо инициализировать анализаторы XMLUnit. Их можно иницилизировать с помощью вызовов System.setProperty или с помощью методов static базового класса XMLUnit.

После инициализации XMLUnit с различными необходимыми анализаторами можно использовать класс Diff, представляющий собой центральный механизм логического сравнения двух XML-документов. В листинге 3 тест testToXML улучшен с помощью XMLUnit:

Листинг 3. Улучшенный тест testToXML

public class XMLReportTest extends TestCase {

 protected void setUp() throws Exception {		 
  XMLUnit.setControlParser(
    "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
  XMLUnit.setTestParser(
    "org.apache.xerces.jaxp.DocumentBuilderFactoryImpl");
  XMLUnit.setSAXParserFactory(
    "org.apache.xerces.jaxp.SAXParserFactoryImpl");
  XMLUnit.setIgnoreWhitespace(true);   
 }

 private Filter[] getFilters(){
  Filter[] fltrs = new Filter[2];
  fltrs[0] = new RegexPackageFilter("java/org");
  fltrs[1] = new SimplePackageFilter("net.");
  return fltrs;
 }

 private Dependency[] getDependencies(){
  Dependency[] deps = new Dependency[2];
  deps[0] = new Dependency("com.acme.resource.Configuration");
  deps[1] = new Dependency("com.acme.xml.Document");
  return deps;
 }

 public void testToXML() {
  BatchDependencyXMLReport report = 
    new BatchDependencyXMLReport(new Date(1165203021718L), 
	  this.getFilters());

  report.addTargetAndDependencies(
    "com.acme.web.Widget", this.getDependencies());
  report.addTargetAndDependencies(
    "com.acme.web.Account", this.getDependencies());

  Diff diff = new Diff(new FileReader(
    new File("./test/conf/report-control.xml")),
    new StringReader(report.toXML()));

  assertTrue("XML was not identical", diff.identical());		
 }
}

Обратите внимание, как инициализируются методы setControlParser, setTestParser и setSAXParserFactory среды XMLUnit. Для этих значений можно использовать любую анализирующую среду, совместимую с JAXP. Также обратите внимание, что метод setIgnoreWhitespace вызывается со значением true - поверьте мне, это спасительное средство! В противном случае придется столкнуться с множеством ошибок в случае, если два документа различаются несоответствием пробелов!

Сравнение с помощью Diff

Класс Diff поддерживает два типа сравнений: identical и similar. Если два сравниваемых документа имеют одинаковую структуру и значения (пробелы игнорируются, если установлен соответстующий флаг), то они считаются идентичными ; если два документа идентичны, они также являются подобными . Обратное утверждение не обязательно будет верным.

Например, в листинге 4 показан простой фрагмент XML, логически подобный XML, представленному в листинге 5. Но эти фрагменты не являются идентичными:

Листинг 4. Фрагмент XML учетной записи

<account>
 <id>3A-00</id>
 <name>acme</name>
</account>

Фрагмент XML в листинге 5 представляет тот же самый логический документ, что и в листинге 4. XMLUnit не считает эти фрагмент идентичными, поскольку элементы name и id поменялись местами.

Листинг 5. Подобный фрагмент XML

<account>
 <name>acme</name>
 <id>3A-00</id>
</account>

Соответственно, можно написать тест для проверки поведения XMLUnit (см. листинг 6):

Листинг 6. Тест для проверки подобия и идентичности

public void testIdenticalAndSimilar() throws Exception {
 String controlXML = "<account><id>3A-00</id><name>acme</name></account>";
 String testXML = "<account><name>acme</name><id>3A-00</id></account>"; 
 Diff diff = new Diff(controlXML, testXML);
 assertTrue(diff.similar());
 assertFalse(diff.identical());
}

Различие между подобными и идентичными XML-документами довольно тонкое; но возможность проверки обоих типов документов может оказаться крайне полезной, например, при тестировании документов, создаваемых различными приложениями или клиентами.

Проверка структуры

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

К счастью, можно снова использовать тестирование, определенное в листинге 3, для проверки структуры документа. При этом текстовые значения элементов и значения атрибутов игнорируются. Это делается с помощью вызова метода overrideDifferenceListener() в классе Diff и предоставления ему класса IgnoreTextAndAttributeValuesDifferenceListener, получаемого из XMLUnit. Отредактированный тест представлен в листинге 7:

Листинг 7. Проверка XML-структуры без значений атрибутов

public void testToXMLFormatOnly() throws Exception{
 BatchDependencyXMLReport report = 
   new BatchDependencyXMLReport(new Date(), this.getFilters());

 report.addTargetAndDependencies(
   "com.acme.web.Widget", this.getDependencies());
 report.addTargetAndDependencies(
   "com.acme.web.Account", this.getDependencies());
 
 Diff diff = new Diff(new FileReader(
   new File("./test/conf/report-control.xml")),
   new StringReader(report.toXML()));

 diff.overrideDifferenceListener(
   new IgnoreTextAndAttributeValuesDifferenceListener());
 assertTrue("XML was not similar", diff.similar());		
}

 
Подобные не значит идентичные!

При использовании класса IgnoreTextAndAttributeValuesDifferenceListener необходимо объявить, что два документа являются similar и не identical. Если по ошибке вызвать identical, будут обрабатываться значения атрибутов.

Разумеется, DTD и XML-схемы упрощают проверку XML-структуры; но иногда в документах нет ссылок на эти схемы. В таких сценариях можно выполнить только проверку структуры. К тому же, если требуется пропустить определенные значения (например, Date) можно реализовать интерфейс DifferenceListener (как сделано в классе IgnoreTextAndAttributeValuesDifferenceListener) и предоставить возможность пользовательской реализации.

XMLUnit с XPath

Для завершения хет-трика XML-тестирования XMLUnit с помощью XPath упрощает проверку определенных фрагментов XML-документа.

Например, используя формат документа, представленного в листинге 1, хотелось бы убедиться, что значение атрибута name первого элемента Class, созданного приложением, соответстует com.acme.web.Widget. Для этого необходимо создать выражение XPath для перехода к точному положению; затем XMLUnit's XMLTestCase предоставляет метод assertXpathExists(), что означает необходимость расширения XMLTestCase.

Листинг 8. Использование XPath для проверки точных XML-значений

public void testToXMLFormatOnly() throws Exception{
 BatchDependencyXMLReport report = 
   new BatchDependencyXMLReport(new Date(), this.getFilters());

 report.addTargetAndDependencies(
   "com.acme.web.Widget", this.getDependencies());
 report.addTargetAndDependencies(
   "com.acme.web.Account", this.getDependencies());
 
 assertXpathExists("//Class[1][@name='com.acme.web.Widget']", 
  report.toXML());	
}

Как видно в листинге 8, XMLUnit вместе с XPath предоставляет удобный механизм для проверки точных аспектов XML-документа вместо выполнения большого тестирования различий. Следует учитывать, что для использования преимуществ XPath в XMLUnit тестирование должно расширять XMLTestCase. Знакомство с XPath также будет полезно!

 
X-что?
XPath или XML Path Language представляет собой язык выражений с адресацией к фрагментам XML-документа на основе древовидного представления. XPath позволяет просматривать XML-документ и упрощает выбор значений документа.
 

Зачем работать больше?

XMLUnit представляет собой инструмент с открытым исходным кодом на основе Java, позволяющий проще тестировать XML-документы и обеспечивающий большую гибкость в сравнении со String. Единственный возможный недостаток использования XMLUnit для тестирования различий заключается в том, что тесты полагаются на файловую систему для загрузки проверяемого документа. Это обуславливает дополнительную зависимость при создании тестов.

Хотя в настоящее время нет новых версий XMLUnit, текущий набор его функций достаточно надежен для обеспечения множества функций тестирования, и при этом совершенно бесплатно!


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=5744