Сначала пару слов о событии. 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://www.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% быть уверенным, что сгенерированный код соответствует схеме, и избавить себя от ошибок, типа «Ой, я забыл перегенерить…» Результатом работы плагина будут сгенерированные классы (осторожно — они огромные):
/** * <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) {...} }
Давайте последовательно рассмотрим каждый метод и подумаем где можно собрать необходимые данные.
-
protected void starting(org.junit.runner.Description description)
— добавим в него инициализацию модели TestCaseResult и создание всех необходимых файлов.
-
protected void succeeded(org.junit.runner.Description description)
— в нем проставим статус OK выполнения нашего теста.
-
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)
— здесь у нас будет условная логика. Если
e instanceOf AssertionViolatedException
, то в тесте произошла ошибка (FAIL), в любом другом случае — тест сломан (ERROR).
-
protected void finished(org.junit.runner.Description description)
— тут сериализуем объект TestCaseResult в xml.
Кроме всего вышеперечисленного, наша рула должна уметь снимать и сохранять скриншоты, что описано в методах:
-
public BufferedImage takeOriginScreenShot(String url)
— снимаем скриншот страницы оригинала по урлу, сохраняем скриншот на файловую систему, линкуем к данным и возвращаем BufferedImage.
-
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. Джоба, которая строит отчет, доступна по адресу.
ссылка на оригинал статьи http://habrahabr.ru/company/yandex/blog/200364/
Добавить комментарий