Интеграционные тесты с Maven, JUnit и Spring

в 20:12, , рубрики: integration testing, java, junit, maven, spring, spring framework, метки: , , , , ,

Юнит тесты это штука хорошая, но, на мой взгляд, полагаться лишь на них нельзя. Иногда очень важно проверить как работают несколько классов, а иногда и слоёв приложения вместе.

У нас приложение активно использует Спринг, а значит и такие интеграционные тесты должны работать с контекстом спринга.

Часто подобные тесты могут занимать много времени, даже если использовать in-memory базу данных (я, кстати проверял почему — у нас много JPA entities с множеством свойств в каждом и инициализация Hibernate, имено она, занимает десятки секунд!)

Значит нам надо сделать так, чтобы по умолчанию эти тесты, в отличие от обычных юнит тестов, не исполнялись во время билда.

Так что план такой:
1. Пишем сервисы, разбитые на два слоя приложения
2. Соединим их Спрингом
3. Напишем тест пользуясь JUnit для взаимодействия этих сервисов (настоящих, без всяких mock)
4. Делаем так, чтобы этот тест не бежал по умолчанию во время билда
5. Profit!

1. Пишем сервисы, разбитые на два слоя приложения

Почему два слоя? Я предпочитаю разбивать бизнес логику на слои (например слой persistency), чтобы код не превращался в спагети. У слоёв есть иерархия — кто кого может вызывать и разделяя слои на разные maven проекты я проверяю, что нет запрещенных зависимостей.

Итак, создаём pom.xml всего приложения с зависимостями, которые нужны всем:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mycompany</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>pom</packaging>

    <name>myapp</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring-version>3.1.0.RELEASE</spring-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring-version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-beans</artifactId>
            <version>${spring-version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring-version}</version>
        </dependency>
    </dependencies>
   
    <modules>
        <module>backend</module>
        <module>frontend</module>
        <module>gui</module>
    </modules>

</project>

В нем три модуля — gui, frontend и backend (два слоя бизнес логики).

Вот pom.xml для frontend:

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.mycompany</groupId>
    <artifactId>myapp</artifactId>
    <version>1.0-SNAPSHOT</version>
  </parent>
  <groupId>com.mycompany</groupId>
  <artifactId>frontend</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>frontend</name>
    <dependencies>
        <dependency>
            <groupId>com.mycompany</groupId>
            <artifactId>backend</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>
</project>

Как видим, он зависит от backend, а всё остальное получает от отцовского pom.

Вот pom.xml для backend:

<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
         xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>com.mycompany</groupId>
        <artifactId>myapp</artifactId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>com.mycompany</groupId>
    <artifactId>backend</artifactId>
    <version>1.0-SNAPSHOT</version>
    <name>backend</name>
</project>

В директории backend/src/main/java/com/mycompany/service/backend создадим:

IBackendService1.java:

package com.mycompany.service.backend;

public interface IBackendService1 {
    String computeSecretString();
}

IBackendService2.java:

package com.mycompany.service.backend;

public interface IBackendService2 {
    int computeSecretNumber();
}

BackendService1.java:

package com.mycompany.service.backend;

public class BackendService1 implements IBackendService1 {
    @Override
    public String computeSecretString() {
        return "James Bond";
    }
}

BackendService2.java:

package com.mycompany.service.backend;

public class BackendService2 implements IBackendService2 {
    @Override
    public int computeSecretNumber() {
        return 7;
    }
}

В директории frontend/src/main/java/com/mycompany/service/frontend создадим:

IFrontendService.java:

package com.mycompany.service.frontend;


public interface IFrontendService {
    String getAgent();
}

FrontendService.java:

package com.mycompany.service.frontend;


import com.mycompany.service.backend.*;

public class FrontendService implements IFrontendService {

    private IBackendService1 backendService1;
    private IBackendService2 backendService2;

    public FrontendService(IBackendService1 backendService1, IBackendService2 backendService2) {
        this.backendService1 = backendService1;
        this.backendService2 = backendService2;
    }

    @Override
    public String getAgent() {
        return backendService1.computeSecretString()+backendService2.computeSecretNumber();
    }
}

2. Соединим их Спрингом

backend/src/main/resources/backend-beans.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd">

    <bean id="names_provider" class="com.mycompany.service.backend.BackendService1"/>
    <bean id="secret_service" class="com.mycompany.service.backend.BackendService2"/>

</beans>

frontend/src/main/resources/frontend-beans.xml:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       ">
    <import resource="classpath:backend-beans.xml"/>

    <bean id="agent_service" class="com.mycompany.service.frontend.FrontendService">
        <constructor-arg index="0" ref="names_provider"/>
        <constructor-arg index="1" ref="secret_service"/>
    </bean>

</beans>
3. Напишем тест пользуясь JUnit для взаимодействия этих сервисов (настоящих, без всяких mock)

В директории frontend/src/test/java/com/mycompany/integration
FrontendServiceTest.java:

package com.mycompany.integration;


import com.mycompany.service.frontend.IFrontendService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import static org.junit.Assert.assertTrue;

@RunWith(SpringJUnit4ClassRunner.class)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@ContextConfiguration(locations = {"classpath:frontend-beans.xml"})
public class FrontendServiceTest {

    @Autowired
    IFrontendService frontendService;

    @Test
    public  void testBond() {
        String agent = frontendService.getAgent();
        assertTrue("It should be Bond", agent.contains("Bond"));
    }

}

Обратите внимание на аннотации:
@RunWith(SpringJUnit4ClassRunner.class) — запускает тесты с «запускателем» Спринга
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) — будет создавать спринговский контекст заново для каждого тест метода
@ContextConfiguration(locations = {"classpath:frontend-beans.xml"}) — откуда брать бины

есть еще @ActiveProfiles(profiles = "local") — если вы используете профайлы.

@Autowired заинжектит вам в тест класс ваши бины. Если у вас есть более одного бина реализующего определенный интерфейс, можно его заменить на @Resource(name = "session-operations")

4. Делаем так, чтобы этот тест не бежал по умолчанию во время билда

Добавляем в pom.xml всего приложения:

  <properties>
        ...
        <systest.package>**/com/mycompany/integration/**</systest.package>
    </properties>

 <build>
       ....    
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.5</version>
                <configuration>
                    <excludes>
                        <exclude>${systest.package}</exclude>
                        <exclude>**/*$*</exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

Теперь тест во время билда не запустится. (Мы также сказали, что inner classes не тесты)

А чтобы была возможность его запускать не только по имени, добавим профайл, в котором этого exclude нет:

 <profiles>
        <profile>
            <id>systest</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-surefire-plugin</artifactId>
                        <version>2.5</version>
                        <configuration>
                            <excludes>
                                <exclude>**/*$*</exclude>
                            </excludes>
                        </configuration>
                    </plugin>
                </plugins>
            </build>
        </profile>
    </profiles>

Теперь все тесты, находящиеся в com.mycompany.integration не будут бежать во время билда, а чтобы их запустить надо установить maven profile «systest», например так:

mvn -P=systest test

5. Profit!

Автор: javax

* - обязательные к заполнению поля