Как стать автором
Обновить
833.15
Яндекс
Как мы делаем Яндекс

Опыт от Яндекса. Как делать свой отчет для автотестов

Время на прочтение15 мин
Количество просмотров22K
Хочу поделиться опытом, о том, как создавать хорошие отчёты об автотестах и одновременно пригласить вас на первое мероприятие Яндекса специально про тестирование.

Сначала пару слов о событии. 30 ноября в Санкт-Петербурге мы проведём Тестовую среду — своё первое мероприятие специально для тестировщиков. Там мы расскажем, как у нас устроено тестирование, что мы сделали для его автоматизации, как работаем с ошибками, данными и графиками и о многом другом. Участие бесплатное, но мест всего 100, поэтому надо успеть зарегистрироваться.

Тестовая среда для нас в первую очередь — площадка для общения. Мы хотим не только рассказать о себе, но и поговорить с участниками о том, как работают они, обменяться знаниями, ответить на какие-то вопросы. Думаем, общих тем будет много, но чтобы вы начали обдумывать их уже сейчас, мы начинаем серию публикаций о тестировании в Яндексе.

Автоматизации тестирования на Тестовой среде будет посвящено несколько докладов, в том числе мой. Итак, начну.
image
Бывают unit-тесты, а бывают высокоуровневые. И когда их количество начинает расти, анализ результатов запусков становится проблемой. Скажите честно, кто из вас не думал сделать свой отчет?

С подробными логами, скриншотами, дампами запросов/ответов и прочей дополнительной информацией (которая, к слову, существенно облегчает обнаружение конкретных причин ошибки). Уверен, что некоторые даже преуспели в этом деле. Проблема заключается в том, что сделать один универсальный отчет для всех типов тестов сложно, а делать отдельный отчет под конкретную задачу — долго. Если, конечно, вы случайно не используете jUnit и Maven. В таком случае сделать простенький отчет для конкретного типа тестов можно за несколько часов. Давайте разберемся, зачем же нам нужен отчет тестов, отличный от xUnit?

Высокоуровневые тесты отличаются от unit-тестов и обладают рядом особенностей:

  1. Они затрагивают гораздо больше функциональности, что затрудняет локализацию проблемы. Так, например, тест через web-интерфейс затрагивает функциональность API, которая в свою очередь затрагивает функциональность базы, которая в свою очередь… ну, вы поняли.
  2. Такие тесты воздействуют на систему через посредников. Это может быть браузер, http-сервер, proxy, third-party системы, в которых в тоже содержится своя логика.
  3. Подобных тестов обычно довольно много и зачастую приходится вводить дополнительную категоризацию. Это могут быть компоненты, области функциональности, критичность.

Все эти факторы существенно замедляют скорость локализации проблемы. Например, вот что может означать ошибка в тесте на 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. Стандартный отчет о прохождении теста будет выглядеть так:

image

Какой вывод можно сделать из отчета тестов?

image

Давайте вместе подумаем, какие данные нам нужны для того, чтобы быстро принять решение о наличие ошибок:

  1. Скриншоты страницы оригинала и кандидата.
  2. Скриншоты дифа (можно, например, все различающиеся пиксели пометить красным)
  3. Исходники страницы оригинала и кандидата.

При наличии таких данных сделать выводы о проблемах будет значительно легче, а значит — дешевле.

Реализация отчета


Для того чтобы построить расширенный отчет тестов, нам нужно пройти три стадии:

  1. Модель. В ней будет содержаться вся информация, необходимая для отображения в отчете.
  2. Адаптер. Он должен собирать всю необходимую информацию из теста в модель.
  3. Генерация отчета. По собранным данным генерируем отчет на основе шаблонов.

Итак, по порядку.

Модель


Для решения этой задачи мы будем использовать 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) {...}
}

Давайте последовательно рассмотрим каждый метод и подумаем где можно собрать необходимые данные.
  • 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
image

3. {uid}-diff.png
image

Генерация отчета


Нам нужно реализовать Maven Report Plugin, который соберет все {{uid}}-testcase.xml-ки в одну и на ее основе сгенерирует html-страничку. Для этого в нашу модель добавим объект-агрегатор TestSuiteResult всех TestCaseResult-ов. Не буду глубоко закапываться в область создания плагинов для Maven — это тема для отдельной статьи. Вместо этого предлагаю рассмотреть уже готовый плагин, который решает нашу задачу.

Итак, у нас есть ScreenShotDifferReport Plugin. Сердцем плагина является метод public void exec (). В нашем случае он должен:
  1. Найти все файлы с данными о прохождении тестов.
    File[] testCasesFiles = listOfFiles(reportDirectory, ".*-testcase\\.xml");
  2. Прочитать их и конвертировать в объекты.
    List<TestCaseResult> testCases = convert(testCasesFile, new Converter<File, TestCaseResult>(){
       public TestCaseResult convert (File file) {
          return JAXB.unmarshall(file, TestCaseResult.xml);
       }  
    });
  3. На основе данных сгенерировать index.html. В качестве шаблонизатора можно использовать freemarker и этот шаблон.
    String source = processTemplate(TEMPLATE_NAME, testCases);
  4. Добавить информацию об этом отчете в группирующий 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.

Плюсы

  1. Бесплатно получаем все возможности jUnit-фреймворка для запуска и организации тестов (параллельный запуск, параметризация, категории).
  2. Четко разделяем данные и представление. Вы можете сделать адаптер на другом языке (например, на python), но использовать тот же плагин для генерации представления. Или использовать разные плагины для одних и тех же данных.
  3. Бесплатно получаем логику доставки отчетов в хранилище (ssh, https, ftp, webdav и т.д.) с помощью Maven Wagon Plugin.
  4. Можем генерировать «частичный отчет». Это достигается благодаря разделению потоков выполнения тестов и построения отчетов. Один поток выполняет тесты (которые генерируют данные), а второй периодически строит отчет.

Минусы

  1. Требуется хорошее знание технологий (XSD, JAXB, jUnit Rules, Maven Reporting Plugin). Если что-то пойдет не так, рискуете потерять много времени.
  2. Довольно сложно тестировать весь цикл построения сложного отчета (от схемы до html)

Рекомендации

  1. Разработка таких систем требует много времени. У нас на разработку первого ушло около 50 литров кофе, двух мешков печенек и 793 нажатий на кнопку Build с учетом анализа технологий и сбора граблей. Сейчас создание отчета под конкретную задачу занимает порядка двух дней. Оцените время, которое вы выиграете, используя этот отчет. Оно должно быть больше.
  2. Наибольший эффект достигается, когда вся команда принимает участие в отсмотре подобных отчетов.


В статье я рассказал об использовании следующих технологий:
1. jUnit, jUnit Rules для реализации Адаптера.
2. JAXB для сериализации/десериализации модели в xml.
3. Maven Reporting Plugins для генерации отчета по готовым данным.

Исходный код примера доступен на github. Джоба, которая строит отчет, доступна по адресу.
Теги:
Хабы:
+54
Комментарии6

Публикации

Информация

Сайт
www.ya.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия