Хочу поделиться опытом, о том, как создавать хорошие отчёты об автотестах и одновременно пригласить вас на первое мероприятие Яндекса специально про тестирование.
Сначала пару слов о событии. 30 ноября в Санкт-Петербурге мы проведём Тестовую среду — своё первое мероприятие специально для тестировщиков. Там мы расскажем, как у нас устроено тестирование, что мы сделали для его автоматизации, как работаем с ошибками, данными и графиками и о многом другом. Участие бесплатное, но мест всего 100, поэтому надо успеть зарегистрироваться.
Тестовая среда для нас в первую очередь — площадка для общения. Мы хотим не только рассказать о себе, но и поговорить с участниками о том, как работают они, обменяться знаниями, ответить на какие-то вопросы. Думаем, общих тем будет много, но чтобы вы начали обдумывать их уже сейчас, мы начинаем серию публикаций о тестировании в Яндексе.
Автоматизации тестирования на Тестовой среде будет посвящено несколько докладов, в том числе мой. Итак, начну.
Бывают unit-тесты, а бывают высокоуровневые. И когда их количество начинает расти, анализ результатов запусков становится проблемой. Скажите честно, кто из вас не думал сделать свой отчет?
С подробными логами, скриншотами, дампами запросов/ответов и прочей дополнительной информацией (которая, к слову, существенно облегчает обнаружение конкретных причин ошибки). Уверен, что некоторые даже преуспели в этом деле. Проблема заключается в том, что сделать один универсальный отчет для всех типов тестов сложно, а делать отдельный отчет под конкретную задачу — долго. Если, конечно, вы случайно не используете jUnit и Maven. В таком случае сделать простенький отчет для конкретного типа тестов можно за несколько часов. Давайте разберемся, зачем же нам нужен отчет тестов, отличный от xUnit?
Высокоуровневые тесты отличаются от unit-тестов и обладают рядом особенностей:
Все эти факторы существенно замедляют скорость локализации проблемы. Например, вот что может означать ошибка в тесте на web-интерфейс «Can not click on element «Search Button»»:
Если же к результатам данного теста добавить скриншот, исходники страницы, сетевой лог и сводку новостей по космической активности в районе датацентра, то указать на конкретную проблему будет гораздо легче, а значит, мы потратим меньше времени. В таком случае возникает и потребность в специфическом отчете с дополнительной информацией.
В качестве подопытного для наших экспериментов возьмем совершенно обычный тест:
Пройдемся по коду:
В таком виде тестом можно пользоваться и без красивого отчета, так как он всегда сравнивает одну и ту же страницу с собой. Но этот тест будет значительно эффективнее, если в него добавить стандартную jUnit параметризацию:
Данные лучше подтягивать из хранилища, к которому имеет доступ человек, использующий тест. Но для наглядности приведенный способ подходит как нельзя лучше.
Итак, представим, что у нас не 2 параметра, а 20, или лучше 200. Стандартный отчет о прохождении теста будет выглядеть так:
Какой вывод можно сделать из отчета тестов?
Давайте вместе подумаем, какие данные нам нужны для того, чтобы быстро принять решение о наличие ошибок:
При наличии таких данных сделать выводы о проблемах будет значительно легче, а значит — дешевле.
Для того чтобы построить расширенный отчет тестов, нам нужно пройти три стадии:
Итак, по порядку.
Для решения этой задачи мы будем использовать xsd-схемы для последующей генерации java-классов с помощью Java JAXB. К счастью, наша модель содержит немного данных и легко описывается схемой.
Схема готова! Теперь осталось сгенерировать по этой схеме классы. Для этого применим мощный maven-jaxb2-plugin. Плюс этого плагина в том, что классы генерируются при каждой компиляции. Таким образом, можно на 100% быть уверенным, что сгенерированный код соответствует схеме, и избавить себя от ошибок, типа «Ой, я забыл перегенерить...» Результатом работы плагина будут сгенерированные классы (осторожно — они огромные):
Классы тоже готовы. Nеперь можно легко и просто сериализовать объекты в xml-файлы:
И зачитывать объекты из xml-файла
Напомню, что адаптер нам необходим для того, чтобы заполнять модель данными из теста во время его выполнения. Для реализации адаптера мы воспользуемся механизмом jUnit Rules, а если быть точнее, то TestWatcher Rule:
Давайте последовательно рассмотрим каждый метод и подумаем где можно собрать необходимые данные.
Кроме всего вышеперечисленного, наша рула должна уметь снимать и сохранять скриншоты, что описано в методах:
Все файлы будем складывать в директорию
После использования 'ScreenShotDifferRule', наш тест практически не изменится:
Теперь с помощью несложной ScreenShotDifferRule после выполнения каждого теста мы будем получать структурированные данные в таком виде:
1. {uid}-testcase.xml
2. {uid}-origin.png
3. {uid}-diff.png
Нам нужно реализовать Maven Report Plugin, который соберет все {{uid}}-testcase.xml-ки в одну и на ее основе сгенерирует html-страничку. Для этого в нашу модель добавим объект-агрегатор TestSuiteResult всех TestCaseResult-ов. Не буду глубоко закапываться в область создания плагинов для Maven — это тема для отдельной статьи. Вместо этого предлагаю рассмотреть уже готовый плагин, который решает нашу задачу.
Итак, у нас есть ScreenShotDifferReport Plugin. Сердцем плагина является метод
Чтобы получить готовый отчет нам нужно выполнить команду
Теперь нужно выполнить инсталляцию всего проекта. Для этого в корне проекта выполним команду
И выполняем команду
Вуаля! После прохождения тестов выполнится фаза site, в рамках которой будет сгенерировано два отчета: SureFire Report и Custom Report.
«Зачем же строить два отчета?» — спросите вы. Дело в том, что механизм jUnit Rules не совершенен. Если в конструкторе теста или в методе параметризации вылетит исключение, то рула не будет создана, а значит, данные для построения отчета не будут собраны. Что в свою очередь означает, что тест в отчет не попадет. Можно усовершенствовать процесс сбора данных с помощью RunListener или Runner, но кажется, что это избыточная логика. Вся информация касательно сломанных тестов есть в SureFire отчете.
Итак, мы научились строить простенькие отчеты с помощью расширений фреймворков jUnit и Maven.
В статье я рассказал об использовании следующих технологий:
1. jUnit, jUnit Rules для реализации Адаптера.
2. JAXB для сериализации/десериализации модели в xml.
3. Maven Reporting Plugins для генерации отчета по готовым данным.
Исходный код примера доступен на github. Джоба, которая строит отчет, доступна по адресу.
Сначала пару слов о событии. 30 ноября в Санкт-Петербурге мы проведём Тестовую среду — своё первое мероприятие специально для тестировщиков. Там мы расскажем, как у нас устроено тестирование, что мы сделали для его автоматизации, как работаем с ошибками, данными и графиками и о многом другом. Участие бесплатное, но мест всего 100, поэтому надо успеть зарегистрироваться.
Тестовая среда для нас в первую очередь — площадка для общения. Мы хотим не только рассказать о себе, но и поговорить с участниками о том, как работают они, обменяться знаниями, ответить на какие-то вопросы. Думаем, общих тем будет много, но чтобы вы начали обдумывать их уже сейчас, мы начинаем серию публикаций о тестировании в Яндексе.
Автоматизации тестирования на Тестовой среде будет посвящено несколько докладов, в том числе мой. Итак, начну.
Бывают unit-тесты, а бывают высокоуровневые. И когда их количество начинает расти, анализ результатов запусков становится проблемой. Скажите честно, кто из вас не думал сделать свой отчет?
С подробными логами, скриншотами, дампами запросов/ответов и прочей дополнительной информацией (которая, к слову, существенно облегчает обнаружение конкретных причин ошибки). Уверен, что некоторые даже преуспели в этом деле. Проблема заключается в том, что сделать один универсальный отчет для всех типов тестов сложно, а делать отдельный отчет под конкретную задачу — долго. Если, конечно, вы случайно не используете jUnit и Maven. В таком случае сделать простенький отчет для конкретного типа тестов можно за несколько часов. Давайте разберемся, зачем же нам нужен отчет тестов, отличный от xUnit?
Высокоуровневые тесты отличаются от unit-тестов и обладают рядом особенностей:
- Они затрагивают гораздо больше функциональности, что затрудняет локализацию проблемы. Так, например, тест через web-интерфейс затрагивает функциональность API, которая в свою очередь затрагивает функциональность базы, которая в свою очередь… ну, вы поняли.
- Такие тесты воздействуют на систему через посредников. Это может быть браузер, http-сервер, proxy, third-party системы, в которых в тоже содержится своя логика.
- Подобных тестов обычно довольно много и зачастую приходится вводить дополнительную категоризацию. Это могут быть компоненты, области функциональности, критичность.
Все эти факторы существенно замедляют скорость локализации проблемы. Например, вот что может означать ошибка в тесте на web-интерфейс «Can not click on element «Search Button»»:
- страница не загрузилась по таймауту;
- на странице отсутствует элемент Search Button;
- элемент Search Button присутствует, но кликнуть на него невозможно;
- на дата центр, в котором крутится сервис, упал метеорит.
Если же к результатам данного теста добавить скриншот, исходники страницы, сетевой лог и сводку новостей по космической активности в районе датацентра, то указать на конкретную проблему будет гораздо легче, а значит, мы потратим меньше времени. В таком случае возникает и потребность в специфическом отчете с дополнительной информацией.
Жил-был тест
В качестве подопытного для наших экспериментов возьмем совершенно обычный тест:
public class ScreenShotDifferTest {
private final long DEVIATION = 20L;
private WebDriver driver = new FirefoxDriver();
public ScreenShooter screenShooter = new ScreenShooter();
@Test
public void originPageShouldBeSameAsModifiedPage() throws Exception {
BufferedImage originScreenShot = screenShooter.takeScreenShot("http://www.yandex.ru", driver);
BufferedImage modifiedScreenShot = screenShooter.takeScreenShot("http://beta.yandex.ru", driver);
long diffPixels = screenShooter.diff(originScreenShot, modifiedScreenShot);
assertThat(diffPixels, lessThan(DEVIATION);
}
@After
public void closeDriver() {
driver.quit();
}
}
Пройдемся по коду:
- инициализируем driver;
- инициализируем screenShooter;
- снимаем скриншот страницы-оригинала;
- снимаем скриншот страницы-кандидата;
- считаем количество различающихся пикселей;
- проверяем, что количество различающихся пикселей не превышает допустимое отклонение;
- закрываем driver.
В таком виде тестом можно пользоваться и без красивого отчета, так как он всегда сравнивает одну и ту же страницу с собой. Но этот тест будет значительно эффективнее, если в него добавить стандартную jUnit параметризацию:
@RunWith(Parameterized.class)
public class ScreenShotDifferTest {
...
private String originPageUrl;
private String modifiedPageUrl;
public ScreenShotDifferTest (String originPageUrl, String modifiedPageUrl) {
this.modifiedPageUrl = modifiedPageUrl;
this.originPageUrl = originPageUrl;
}
@Parameterized.Parameters(name = "{0}")
public static Collection<Object[]> readUrlPairs () {
return Arrays.asList(
new Object[]{"Yandex Main Page", "http://www.yandex.ru/", "http://beta.yandex.ru/"},
new Object[]{"Yandex.Market Main Page", "http://market.yandex.ru/", "http://beta.market.yandex.ru/"}
);
}
...
}
Данные лучше подтягивать из хранилища, к которому имеет доступ человек, использующий тест. Но для наглядности приведенный способ подходит как нельзя лучше.
Итак, представим, что у нас не 2 параметра, а 20, или лучше 200. Стандартный отчет о прохождении теста будет выглядеть так:
Какой вывод можно сделать из отчета тестов?
Давайте вместе подумаем, какие данные нам нужны для того, чтобы быстро принять решение о наличие ошибок:
- Скриншоты страницы оригинала и кандидата.
- Скриншоты дифа (можно, например, все различающиеся пиксели пометить красным)
- Исходники страницы оригинала и кандидата.
При наличии таких данных сделать выводы о проблемах будет значительно легче, а значит — дешевле.
Реализация отчета
Для того чтобы построить расширенный отчет тестов, нам нужно пройти три стадии:
- Модель. В ней будет содержаться вся информация, необходимая для отображения в отчете.
- Адаптер. Он должен собирать всю необходимую информацию из теста в модель.
- Генерация отчета. По собранным данным генерируем отчет на основе шаблонов.
Итак, по порядку.
Модель
Для решения этой задачи мы будем использовать xsd-схемы для последующей генерации java-классов с помощью Java JAXB. К счастью, наша модель содержит немного данных и легко описывается схемой.
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema attributeFormDefault="unqualified" elementFormDefault="unqualified" xmlns:xsd="http://www.w3.org/2001/XMLSchema"
xmlns:ns="urn:report.examples.qatools.yandex.ru" targetNamespace="urn:report.examples.qatools.yandex.ru" version="2.1">
<xsd:element name="testCaseResult" type="ns:TestCaseResult"/> <!--Результат выполнения теста-->
<xsd:complexType name="TestCaseResult">
<xsd:sequence>
<xsd:element name="description" type="xsd:string"/> <!--Чтобы понять что именно проверялось в тесте-->
<xsd:element name="origin" type="ns:ScreenShotData" nillable="false"/> <!--Данные страницы эталона (обычно эталон или продакшен)-->
<xsd:element name="modified" type="ns:ScreenShotData" nillable="false"/> <!--Данные страницы кандидата на релиз (обычно берется бета)-->
<xsd:element name="diff" type="ns:DiffData" nillable="false"/> <!--Данные различий двух скриншотов-->
<xsd:element name="message" type="xsd:string"/> <!--Сообщение об ошибке, если она есть-->
</xsd:sequence>
<xsd:attribute name="uid" type="xsd:string"/> <!--ID-шник теста-->
<xsd:attribute name="title" type="xsd:string"/> <!--Краткое название теста, чтобы понимать что проверялось-->
<xsd:attribute name="status" type="ns:Status"/> <!--Статус завершения теста-->
</xsd:complexType>
<xsd:complexType name="ScreenShotData">
<xsd:sequence>
<xsd:element name="pageUrl" type="xsd:string"/> <!--Урл страницы, с которой снят скриншот-->
<xsd:element name="fileName" type="xsd:string"/> <!--Название файла со скриншотом-->
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="DiffData">
<xsd:sequence>
<xsd:element name="pixels" type="xsd:long" default="0"/> <!--Количество различающихся пикселей-->
<xsd:element name="fileName" type="xsd:string"/><!--Название файла с дифом-->
</xsd:sequence>
</xsd:complexType>
<xsd:simpleType name="Status">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="OK"/>
<xsd:enumeration value="FAIL"/>
<xsd:enumeration value="ERROR"/>
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>
Схема готова! Теперь осталось сгенерировать по этой схеме классы. Для этого применим мощный maven-jaxb2-plugin. Плюс этого плагина в том, что классы генерируются при каждой компиляции. Таким образом, можно на 100% быть уверенным, что сгенерированный код соответствует схеме, и избавить себя от ошибок, типа «Ой, я забыл перегенерить...» Результатом работы плагина будут сгенерированные классы (осторожно — они огромные):
TestCaseReport
/**
* <p>Java class for TestCaseResult complex type.
*
* <p>The following schema fragment specifies the expected content contained within this class.
*
* <pre>
* <complexType name="TestCaseResult">
* <complexContent>
* <restriction base="{http://www.w3.org/2001/XMLSchema}anyType">
* <sequence>
* <element name="message" type="{http://www.w3.org/2001/XMLSchema}string"/>
* <element name="description" type="{http://www.w3.org/2001/XMLSchema}string"/>
* <element name="origin" type="{urn:report.examples.qatools.yandex.ru}ScreenShotData"/>
* <element name="modified" type="{urn:report.examples.qatools.yandex.ru}ScreenShotData"/>
* <element name="diff" type="{urn:report.examples.qatools.yandex.ru}DiffData"/>
* </sequence>
* <attribute name="uid" type="{http://www.w3.org/2001/XMLSchema}string" />
* <attribute name="title" type="{http://www.w3.org/2001/XMLSchema}string" />
* <attribute name="status" type="{urn:report.examples.qatools.yandex.ru}Status" />
* </restriction>
* </complexContent>
* </complexType>
* </pre>
*
*
*/
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "TestCaseResult", propOrder = {
"message",
"description",
"origin",
"modified",
"diff"
})
public class TestCaseResult {
@XmlElement(required = true)
protected String message;
@XmlElement(required = true)
protected String description;
@XmlElement(required = true)
protected ScreenShotData origin;
@XmlElement(required = true)
protected ScreenShotData modified;
@XmlElement(required = true)
protected DiffData diff;
@XmlAttribute(name = "uid")
protected String uid;
@XmlAttribute(name = "title")
protected String title;
@XmlAttribute(name = "status")
protected Status status;
/**
* Gets the value of the message property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getMessage() {
return message;
}
/**
* Sets the value of the message property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setMessage(String value) {
this.message = value;
}
/**
* Gets the value of the description property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getDescription() {
return description;
}
/**
* Sets the value of the description property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setDescription(String value) {
this.description = value;
}
/**
* Gets the value of the origin property.
*
* @return
* possible object is
* {@link ScreenShotData }
*
*/
public ScreenShotData getOrigin() {
return origin;
}
/**
* Sets the value of the origin property.
*
* @param value
* allowed object is
* {@link ScreenShotData }
*
*/
public void setOrigin(ScreenShotData value) {
this.origin = value;
}
/**
* Gets the value of the modified property.
*
* @return
* possible object is
* {@link ScreenShotData }
*
*/
public ScreenShotData getModified() {
return modified;
}
/**
* Sets the value of the modified property.
*
* @param value
* allowed object is
* {@link ScreenShotData }
*
*/
public void setModified(ScreenShotData value) {
this.modified = value;
}
/**
* Gets the value of the diff property.
*
* @return
* possible object is
* {@link DiffData }
*
*/
public DiffData getDiff() {
return diff;
}
/**
* Sets the value of the diff property.
*
* @param value
* allowed object is
* {@link DiffData }
*
*/
public void setDiff(DiffData value) {
this.diff = value;
}
/**
* Gets the value of the uid property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getUid() {
return uid;
}
/**
* Sets the value of the uid property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setUid(String value) {
this.uid = value;
}
/**
* Gets the value of the title property.
*
* @return
* possible object is
* {@link String }
*
*/
public String getTitle() {
return title;
}
/**
* Sets the value of the title property.
*
* @param value
* allowed object is
* {@link String }
*
*/
public void setTitle(String value) {
this.title = value;
}
/**
* Gets the value of the status property.
*
* @return
* possible object is
* {@link Status }
*
*/
public Status getStatus() {
return status;
}
/**
* Sets the value of the status property.
*
* @param value
* allowed object is
* {@link Status }
*
*/
public void setStatus(Status value) {
this.status = value;
}
}
Классы тоже готовы. Nеперь можно легко и просто сериализовать объекты в xml-файлы:
TestCaseResult testCaseResult = ...
JAXB.marshal(testCaseResult, file);
И зачитывать объекты из xml-файла
TestCaseResult testCaseResult = JAXB.unmarshal(file, TestCaseResult.class)
Адаптер
Напомню, что адаптер нам необходим для того, чтобы заполнять модель данными из теста во время его выполнения. Для реализации адаптера мы воспользуемся механизмом jUnit Rules, а если быть точнее, то TestWatcher Rule:
public abstract class TestWatcher implements org.junit.rules.TestRule {
//обязательно вызывается перед началом теста
protected void starting(org.junit.runner.Description description) {...}
//этот метод вызывается в случае успешного завершения теста
protected void succeeded(org.junit.runner.Description description) {...}
//этот метод вызывается, если //вы используете// !!(сработает)!! assumeThat()
protected void skipped(org.junit.internal.AssumptionViolatedException e, org.junit.runner.Description description) {...}
//этот метод будет вызван в случае возникновения ошибки в тесте
protected void failed(java.lang.Throwable e, org.junit.runner.Description description) {...}
//обязательно вызывается после завершения теста
protected void finished(org.junit.runner.Description description) {...}
}
Давайте последовательно рассмотрим каждый метод и подумаем где можно собрать необходимые данные.
— добавим в него инициализацию модели TestCaseResult и создание всех необходимых файлов.protected void starting(org.junit.runner.Description description)
— в нем проставим статус OK выполнения нашего теста.protected void succeeded(org.junit.runner.Description description)
— нас этот метод никак не интересует. Его можно оставить без изменения.protected void skipped(org.junit.internal.AssumptionViolatedException e, org.junit.runner.Description description)
— здесь у нас будет условная логика. Еслиprotected void failed(java.lang.Throwable e, org.junit.runner.Description description)
, то в тесте произошла ошибка (FAIL), в любом другом случае — тест сломан (ERROR).e instanceOf AssertionViolatedException
— тут сериализуем объект TestCaseResult в xml.protected void finished(org.junit.runner.Description description)
Кроме всего вышеперечисленного, наша рула должна уметь снимать и сохранять скриншоты, что описано в методах:
— снимаем скриншот страницы оригинала по урлу, сохраняем скриншот на файловую систему, линкуем к данным и возвращаем BufferedImage.public BufferedImage takeOriginScreenShot(String url)
— те же самые операции, только для страницы кандидата.public BufferedImage takeModifiedScreenShot(String url)
— получаем дифф двух скриншотов, сохраняем на файловую систему, линкуем к данным и возвращаем объект с информацией о различиях.public DiffData diff(BufferedImage original, BufferedImage modified)
Все файлы будем складывать в директорию
target/site/custom
, так как она является дефолтной для отчетов. После использования 'ScreenShotDifferRule', наш тест практически не изменится:
@RunWith(Parameterized.class)
public class ScreenShotDifferTest {
private String originPageUrl;
private String modifiedPageUrl;
...
@Rule
public ScreenShotDifferRule screenShotDiffer = new ScreenShotDifferRule(driver);
public ScreenShotDifferTest(String title, String originPageUrl, String modifiedPageUrl) {
this.modifiedPageUrl = modifiedPageUrl;
this.originPageUrl = originPageUrl;
}
...
@Test
public void originShouldBeSameAsModified() throws Exception {
BufferedImage originScreenShot = screenShotDiffer.takeOriginScreenShot(originPageUrl);
BufferedImage modifiedScreenShot = screenShotDiffer.takeModifiedScreenShot(modifiedPageUrl);
long diffPixels = screenShotDiffer.diff(originScreenShot, modifiedScreenShot);
assertThat(diffPixels, lessThan((long) 20));
}
...
}
Теперь с помощью несложной ScreenShotDifferRule после выполнения каждого теста мы будем получать структурированные данные в таком виде:
1. {uid}-testcase.xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<testCaseResult status="OK" title="originShouldBeSameAsModified[0](ru.yandex.qatools.examples.report.ScreenShotDifferTest)" uid="ru.yandex.qatools.examples.report.ScreenShotDifferTest.originShouldBeSameAsModified[0]">
<origin>
<pageUrl>http://www.yandex.ru/</pageUrl>
<fileName>{uid}-origin.png</fileName>
</origin>
<modified>
<pageUrl>http://www.yandex.ru/</pageUrl>
<fileName>{uid}-modified.png</fileName>
</modified>
<diff>
<pixels>0</pixels>
<fileName>{uid}-diff.png</fileName>
</diff>
</testCaseResult>
2. {uid}-origin.png
3. {uid}-diff.png
Генерация отчета
Нам нужно реализовать Maven Report Plugin, который соберет все {{uid}}-testcase.xml-ки в одну и на ее основе сгенерирует html-страничку. Для этого в нашу модель добавим объект-агрегатор TestSuiteResult всех TestCaseResult-ов. Не буду глубоко закапываться в область создания плагинов для Maven — это тема для отдельной статьи. Вместо этого предлагаю рассмотреть уже готовый плагин, который решает нашу задачу.
Итак, у нас есть ScreenShotDifferReport Plugin. Сердцем плагина является метод
public void exec ()
. В нашем случае он должен: - Найти все файлы с данными о прохождении тестов.
File[] testCasesFiles = listOfFiles(reportDirectory, ".*-testcase\\.xml");
- Прочитать их и конвертировать в объекты.
List<TestCaseResult> testCases = convert(testCasesFile, new Converter<File, TestCaseResult>(){ public TestCaseResult convert (File file) { return JAXB.unmarshall(file, TestCaseResult.xml); } });
- На основе данных сгенерировать index.html. В качестве шаблонизатора можно использовать freemarker и этот шаблон.
String source = processTemplate(TEMPLATE_NAME, testCases);
- Добавить информацию об этом отчете в группирующий maven-отчет.
Sink sink = new Sink(); sink.setHtml(source); sink.close();((
Чтобы получить готовый отчет нам нужно выполнить команду
mvn clean install
. Для простоты можно выкачать проект github.com/yandex-qatools/tests-report-example и выполнить команду для него. В результате выполнения команды в модуле tests-report-example в директории target/site/ вы увидите отчет по проекту.Проверка результата
Теперь нужно выполнить инсталляцию всего проекта. Для этого в корне проекта выполним команду
mvn clean install
. После её выполнения мы получим артефакты, готовые для использования. Подключаем наш новоиспеченный плагин к проекту автотестов вместе со стандартным surefire-плагином. <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-site-plugin</artifactId>
<version>3.2</version>
<configuration>
<reportPlugins>
<plugin>
<groupId>ru.yandex.qatools.examples</groupId>
<artifactId>custom-report-plugin</artifactId>
<version>${project.version}</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>2.14.1</version>
</plugin>
</reportPlugins>
</configuration>
</plugin>
И выполняем команду
mvn clean site
.Вуаля! После прохождения тестов выполнится фаза site, в рамках которой будет сгенерировано два отчета: SureFire Report и Custom Report.
«Зачем же строить два отчета?» — спросите вы. Дело в том, что механизм jUnit Rules не совершенен. Если в конструкторе теста или в методе параметризации вылетит исключение, то рула не будет создана, а значит, данные для построения отчета не будут собраны. Что в свою очередь означает, что тест в отчет не попадет. Можно усовершенствовать процесс сбора данных с помощью RunListener или Runner, но кажется, что это избыточная логика. Вся информация касательно сломанных тестов есть в SureFire отчете.
Итог
Итак, мы научились строить простенькие отчеты с помощью расширений фреймворков jUnit и Maven.
Плюсы
- Бесплатно получаем все возможности jUnit-фреймворка для запуска и организации тестов (параллельный запуск, параметризация, категории).
- Четко разделяем данные и представление. Вы можете сделать адаптер на другом языке (например, на python), но использовать тот же плагин для генерации представления. Или использовать разные плагины для одних и тех же данных.
- Бесплатно получаем логику доставки отчетов в хранилище (ssh, https, ftp, webdav и т.д.) с помощью Maven Wagon Plugin.
- Можем генерировать «частичный отчет». Это достигается благодаря разделению потоков выполнения тестов и построения отчетов. Один поток выполняет тесты (которые генерируют данные), а второй периодически строит отчет.
Минусы
- Требуется хорошее знание технологий (XSD, JAXB, jUnit Rules, Maven Reporting Plugin). Если что-то пойдет не так, рискуете потерять много времени.
- Довольно сложно тестировать весь цикл построения сложного отчета (от схемы до html)
Рекомендации
- Разработка таких систем требует много времени. У нас на разработку первого ушло около 50 литров кофе, двух мешков печенек и 793 нажатий на кнопку Build с учетом анализа технологий и сбора граблей. Сейчас создание отчета под конкретную задачу занимает порядка двух дней. Оцените время, которое вы выиграете, используя этот отчет. Оно должно быть больше.
- Наибольший эффект достигается, когда вся команда принимает участие в отсмотре подобных отчетов.
В статье я рассказал об использовании следующих технологий:
1. jUnit, jUnit Rules для реализации Адаптера.
2. JAXB для сериализации/десериализации модели в xml.
3. Maven Reporting Plugins для генерации отчета по готовым данным.
Исходный код примера доступен на github. Джоба, которая строит отчет, доступна по адресу.