Статьи

Покрытие кода модульными и интеграционными тестами

Недавно в одном из домашних проектов я решил создать автоматизированные тесты пользовательского интерфейса (интеграции), а также обычные модульные тесты. Я хотел, чтобы все это было интегрировано в мою сборку Maven с отчетами о покрытии кода, чтобы я мог получить представление об областях с недостаточным охватом тестированием. Вместо того, чтобы просто публиковать исходный код проекта, я собрал простой пример, чтобы продемонстрировать, как я получил всю эту настройку; так что если вы хотите интегрировать maven , junit , webdriver (теперь selenium) и emma — читайте дальше, чтобы узнать, как я это сделал .

Прежде всего, весь исходный код для этого доступен на github: https://github.com/activelylazy/coverage-example . Я покажу ключевые фрагменты, но очевидно, что здесь пропущено много деталей, которые (надеюсь) не имеют значения.

Пример приложения

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

Как это работает

Стартовая страница — это простая ссылка на страницу приветствия:

1
2
<h1>Example app</h1>
<p>See the <a id="messageLink" href="helloWorld.html">message</a></p>

Страница Hello World просто отображает сообщение:

1
2
<h1>Example app</h1>
<p id="message"><c:out value="${message}"/></p>

Контроллер hello world отображает представление, передавая сообщение:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public class HelloWorldController extends ParameterizableViewController {
    // Our message factory
    private MessageFactory messageFactory;
    @Override
    protected ModelAndView handleRequestInternal(HttpServletRequest request,
        HttpServletResponse response) throws Exception {
        // Get the success view
        ModelAndView mav = super.handleRequestInternal(request, response);
        // Add our message
        mav.addObject("message",messageFactory.createMessage());
        return mav;
    }
    @Autowired
    public void setMessageFactory(MessageFactory messageFactory) {
        this.messageFactory = messageFactory;
    }
}

Наконец, MessageFactory просто возвращает жестко закодированное сообщение:

1
2
3
public String createMessage() {
    return "Hello world";
}

Юнит тест

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

01
02
03
04
05
06
07
08
09
10
11
12
public class MessageFactoryTest {
    // The message factory
    private MessageFactory messageFactory;
    @Test
    public void testCreateMessage() {
        assertEquals("Hello world",messageFactory.createMessage());
    }
    @Autowired
    public void setMessageFactory(MessageFactory messageFactory) {
        this.messageFactory = messageFactory;
    }
}

Сложение

Для сборки этого и запуска модульного теста достаточно простого файла maven pom. На данный момент у нас есть работающее приложение с модульным тестом основных функций (таких как оно), которые мы можем создать и запустить.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>helloworld</artifactId>
    <packaging>war</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>helloworld Maven Webapp</name>
    <build>
        <finalName>helloworld</finalName>
    </build>
    <dependencies>
        ...omitted...
    </dependencies>
</project>

Покрытие кода

Теперь давайте интегрируем Эмму, чтобы мы могли получить некоторые отчеты о покрытии кода. Во-первых, мы определяем новый профиль Maven, это позволяет нам контролировать, будем ли мы использовать emma в любой данной сборке.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
<profile>
    <id>with-emma</id>
    <build>
        <plugins>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>emma-maven-plugin</artifactId>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <id>instrument</id>
                        <phase>process-test-classes</phase>
                        <goals>
                            <goal>instrument</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>

Это просто вызывает цель «инструмента» во время фазы Maven «процесс-тест-классы»; то есть, как только мы скомпилировали наши файлы классов, используйте emma для их инструментов. Мы можем запустить это, вызвав maven с новым профилем:

mvn clean install -Pwith-emma

После завершения сборки мы можем запустить Emma для генерации отчетов о покрытии кода:
В Windows:

java -cp %USERPROFILE%/.m2/repository/emma/emma/2.0.5312/emma-2.0.5312.jar emma report -r xml,html -in coverage.ec -in target/coverage.em

В Linux:

java -cp ~/.m2/repository/emma/emma/2.0.5312/emma-2.0.5312.jar emma report -r xml,html -in coverage.ec -in target/coverage.em

Теперь мы можем просмотреть отчет о покрытии в формате HTML в cover / index.html. На данный момент, это показывает, что у нас есть 50% тестовое покрытие (по классам). MessageFactory полностью покрыт, но HelloWorldController вообще не имеет никаких тестов.

Интеграционный тест

Чтобы протестировать наш контроллер и JSP, мы будем использовать WebDriver для создания простого интеграционного теста; это тест JUnit, который запускает браузер.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class HelloWorldIntegrationTest {
    // The webdriver
    private static WebDriver driver;
    @BeforeClass
    public static void initWebDriver() {
        driver = new FirefoxDriver();
    }
    @AfterClass
    public static void stopSeleniumClent() {
        try {
            driver.close();
            driver.quit();
        } catch( Throwable t ) {
            // Catch error & log, not critical for tests
            System.err.println("Error stopping driver: "+t.getMessage());
            t.printStackTrace(System.err);
        }
    }
    @Test
    public void testHelloWorld() {
        // Start from the homepage
        driver.get("http://localhost:9080/helloworld/");
        HomePage homePage = new HomePage(driver);
        HelloWorldPage helloWorldPage = homePage.clickMessageLink();
        assertEquals("Hello world",helloWorldPage.getMessage());
    }
}

Строки 4-18 просто запускают Web Driver перед тестом и закрывают его (закрывая окно браузера) после завершения теста.
В строке 22 мы переходим на домашнюю страницу с жестко заданным URL-адресом.
В строке 23 мы инициализируем наш объект страницы веб-драйвера для домашней страницы. Это включает в себя все детали работы страницы, позволяя тесту функционально взаимодействовать со страницей, не беспокоясь о механике (какие элементы использовать и т. Д.).
В строке 24 мы используем объект домашней страницы, чтобы щелкнуть ссылку «сообщение»; это переходит на страницу приветствия.
В строке 25 мы подтверждаем, что сообщение, отображаемое на странице приветствия, соответствует ожиданиям.
Примечание. Я использую объекты страницы для отделения спецификации теста (что делать) от реализации теста (как это сделать). Подробнее о том, почему это важно, смотрите, как не допускать ломкости тестов .

домашняя страница

Объект домашней страницы довольно прост:

1
2
3
4
public HelloWorldPage clickMessageLink() {
    driver.findElement(By.id("messageLink")).click();
    return new HelloWorldPage(driver);
}

HelloWorldPage

Страница «Привет, мир» одинаково проста:

1
2
3
public String getMessage() {
    return driver.findElement(By.id("message")).getText();
}

Выполнение интеграционного теста

Чтобы запустить интеграционный тест во время нашей сборки Maven, нам нужно внести несколько изменений. Во-первых, нам нужно исключить интеграционные тесты из этапа модульных тестов:

01
02
03
04
05
06
07
08
09
10
11
12
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    ...
    <configuration>
        ...
        <excludes>
            <exclude>**/*IntegrationTest.java</exclude>
            <exclude>**/common/*</exclude>
        </excludes>
    </configuration>
</plugin>

Затем мы определяем новый профиль, чтобы мы могли дополнительно запускать интеграционные тесты:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
<profile>
    <id>with-integration-tests</id>
    <build>
        <plugins>
            <plugin>
                <groupId>org.mortbay.jetty</groupId>
                <artifactId>maven-jetty-plugin</artifactId>
                <version>6.1.22</version>
                <configuration>
                    <scanIntervalSeconds>5</scanIntervalSeconds>
                    <stopPort>9966</stopPort>
                    <stopKey>foo</stopKey>
                    <connectors>
                        <connector implementation="org.mortbay.jetty.nio.SelectChannelConnector">
                            <port>9080</port>
                            <maxIdleTime>60000</maxIdleTime>
                        </connector>
                    </connectors>
                </configuration>
                <executions>
                    <execution>
                        <id>start-jetty</id>
                        <phase>pre-integration-test</phase>
                        <goals>
                            <goal>run</goal>
                        </goals>
                        <configuration>
                            <daemon>true</daemon>
                        </configuration>
                    </execution>
                    <execution>
                        <id>stop-jetty</id>
                        <phase>post-integration-test</phase>
                        <goals>
                            <goal>stop</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.5</version>
                <inherited>true</inherited>
                <executions>
                    <execution>
                        <id>integration-tests</id>
                        <phase>integration-test</phase>
                        <goals>
                            <goal>test</goal>
                        </goals>
                        <configuration>
                            <excludes>
                                <exclude>**/common/*</exclude>
                            </excludes>
                            <includes>
                                <include>**/*IntegrationTest.java</include>
                            </includes>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</profile>
<Профиль>

<Идентификатор> с-интеграционным-тестов </ ID>

<Сборка>

<Плагины>

<Плагин>

<Идентификатор_группы> org.mortbay.jetty </ идентификатор_группы>

<Артефакт> Maven-мол-плагин </ артефакт>

<Версия> 6.1.22 </ версия>

<Конфигурация>

<ScanIntervalSeconds> 5 </ scanIntervalSeconds>

<StopPort> 9966 </ stopPort>

<StopKey> Foo </ stopKey>

<Соединители>

<реализация коннектора = ”org.mortbay.jetty.nio.SelectChannelConnector”>

<Порт> $ {test.server.port} </ порт>

<MaxIdleTime> 60000 </ maxIdleTime>

</ Разъем>

</ Разъемы>

</ Конфигурация>

<казни>

<Исполнение>

<Идентификатор> старт-причал </ ID>

<Фаза> предварительной интеграции тест </ фаза>

<цели>

<Цель> запустить </ цель>

</ Цели>

<Конфигурация>

<Демон> True </ демон>

</ Конфигурация>

</ Выполнение>

<Исполнение>

<Идентификатор> стоп-молы </ ID>

<Фаза> после интеграции тест </ фаза>

<цели>

<Цель> остановка </ цель>

</ Цели>

</ Выполнение>

</ Расстрелы>

</ Плагин>

<Плагин>

<Идентификатор_группы> org.apache.maven.plugins </ идентификатор_группы>

<Артефакт> Maven-безошибочный-плагин </ артефакт>

<Версия> 2.5 </ версия>

<Унаследовал> True </ наследуются>

<казни>

<Исполнение>

<Идентификатор> Интеграция-тесты </ ID>

<Фаза> интеграция тест </ фаза>

<цели>

<Цель> тест </ цель>

</ Цели>

<Конфигурация>

<Исключает>

<Исключить> ** / общие / * </ исключить>

</ Исключает>

<Включает>

<Включают> ** / * IntegrationTest.java </ включают>

</ Включает в себя>

</ Конфигурация>

</ Выполнение>

</ Расстрелы>

</ Плагин>

</ Плагины>

</ Сборки>

</ Профиль>

Это может показаться сложным, но на самом деле мы просто настраиваем Jetty для запуска во время наших интеграционных тестов; затем настройте сами, как запускать интеграционные тесты.
В строках 9-19 настройте причал — порт для запуска и способ его остановки.
Строки 21-30 настраивают причал для работы во время фазы «предварительной интеграции» сборки maven.
Строки 31-37 конфигурируют причал, который должен быть остановлен на этапе «тестирования после интеграции» сборки maven.
В строках 40-62 мы снова используем подключаемый модуль maven-surefire-plugin, на этот раз для запуска на этапе «интеграция-тест» сборки, только с нашими классами интеграционного тестирования.

Мы можем запустить эту сборку с:

mvn clean install -Pwith-emma -Pwith-integration-tests

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

Теперь мы можем создавать наше приложение, запускать модульные и интеграционные тесты, собирать комбинированные отчеты о покрытии кода. Если мы повторно запустим отчет emma и проверим покрытие кода, мы увидим, что у нас есть 100% тестовое покрытие — поскольку контроллер также покрывается тестами.

вопросы

Какие нерешенные проблемы с этим, какие дополнительные расширения могут быть сделаны?

  • Сборка создает инструментированный WAR — это означает, что вам нужно запустить вторую сборку без emma, чтобы получить готовую сборку.
  • Тест интеграции жестко кодирует порт, на котором настроен Jetty; Это означает, что тесты не могут быть запущены непосредственно в Eclipse. Можно передать этот порт, по умолчанию, скажем, 8080, так что интеграционные тесты могут без проблем запускаться в Eclipse через сборку maven.
  • При работе на сервере сборки вы, вероятно, не хотите, чтобы Firefox появлялся случайно (если X даже установлен); так что запуск xvfb — хорошая идея. Можно настроить maven для запуска и остановки xvfb до и после интеграционных тестов.

Ссылка: покрытие кода с модульными и интеграционными тестами и от нашего партнера JCG Дейва в блоге Actively Lazy

Статьи по Теме :