Статьи

Тестирование Java EE 6 с Arquillian (включая JPA, EJB, Bean Validation и CDI)

В течение очень долгого времени я слышал, как много людей говорили хорошие слова об Аркиллиане . Пока я читал статьи о его использовании, я не смог найти ту, которая охватывала бы некоторые аспекты, которые я считаю важными, все в одной статье. Конечно, я не выглядел достаточно усердно.

Вопросы, которые я хотел бы затронуть:

  • Использование JPA. Я просто использую EclipseLink здесь,
  • Использование базы данных в памяти,
  • Использование инъекции CDI,
  • Использование EJB, скажем, локальный сессионный компонент без сохранения состояния,
  • Использование валидации JSR-303,
  • Использование (встроенного) Glassfish для интеграционного тестирования.

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

Настройте pom.xml

Конечно, нам нужно настроить проект на использование Arquillian, а также использовать EclipseLink в качестве поставщика сохраняемости. Но имейте в виду, что мы также решили до этого, что мы хотим использовать базу данных в памяти. Возможно, вы захотите включить свою зависимость здесь, в этот pom.xml. Однако я решил использовать Derby, который доступен в стандартном пути к классам Oracle Java SDK.

В любом случае, pom.xml будет выглядеть так:

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
 
 
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>inout</artifactId>
        <groupId>id.co.dwuysan</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
 
    <groupId>id.co.dwuysan</groupId>
    <artifactId>inout-ejb</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>ejb</packaging>
 
    <name>inout-ejb</name>
 
    <properties>
        <endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <netbeans.hint.deploy.server>gfv3ee6</netbeans.hint.deploy.server>
    </properties>
 
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.jboss.arquillian</groupId>
                <artifactId>arquillian-bom</artifactId>
                <version>1.0.0.Final</version>
                <scope>import</scope>
                <type>pom</type>
            </dependency>
        </dependencies>
    </dependencyManagement>
 
    <dependencies>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>eclipselink</artifactId>
            <version>2.3.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>javax.persistence</artifactId>
            <version>2.0.3</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.eclipse.persistence</groupId>
            <artifactId>org.eclipse.persistence.jpa.modelgen.processor</artifactId>
            <version>2.3.2</version>
            <scope>provided</scope>
        </dependency>
 
        <!-- test -->
 
        <dependency>
            <groupId>org.jboss.arquillian</groupId>
            <artifactId>arquillian-bom</artifactId>
            <version>1.0.3.Final</version>
            <type>pom</type>
        </dependency>
        <dependency>
            <groupId>org.glassfish.main.extras</groupId>
            <artifactId>glassfish-embedded-all</artifactId>
            <version>3.1.2.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.container</groupId>
            <artifactId>arquillian-glassfish-embedded-3.1</artifactId>
            <version>1.0.0.CR3</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.arquillian.junit</groupId>
            <artifactId>arquillian-junit-container</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
 
        <!-- environment requirement -->
        <dependency>
            <groupId>javax</groupId>
            <artifactId>javaee-api</artifactId>
            <version>6.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.12.4</version>
                <configuration>
                    <argLine>-XX:-UseSplitVerifier</argLine>
                    <systempropertyvariables>
                        <java.util.logging.config.file>
                            ${basedir}/src/test/resources/logging.properties
                        </java.util.logging.config.file>
                    </systempropertyvariables>
                    <systemProperties>
                        <property>
                            <name>derby.stream.error.file</name>
                            <value>target/derby.log</value>
                        </property>
                    </systemProperties>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>2.3.2</version>
                <configuration>
                    <source>1.7</source>
                    <target>1.7</target>
                    <compilerArguments>
                        <endorseddirs>${endorsed.dir}</endorseddirs>
                    </compilerArguments>
                    <showDeprecation>true</showDeprecation>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-ejb-plugin</artifactId>
                <version>2.3</version>
                <configuration>
                    <ejbVersion>3.1</ejbVersion>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>2.1</version>
                <executions>
                    <execution>
                        <phase>validate</phase>
                        <goals>
                            <goal>copy</goal>
                        </goals>
                        <configuration>
                            <outputDirectory>${endorsed.dir}</outputDirectory>
 
                            <silent>true</silent>
                            <artifactItems>
                                <artifactItem>
                                    <groupId>javax</groupId>
                                    <artifactId>javaee-endorsed-api</artifactId>
                                    <version>6.0</version>
                                    <type>jar</type>
                                </artifactItem>
                            </artifactItems>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>java.net</id>
            <url>http://download.java.net/maven/</url>
        </repository>
        <repository>
            <id>JBOSS_NEXUS</id>
        </repository>
        <repository>
            <id>eclipselink</id>
            <layout>default</layout>
            <name>Repository for library EclipseLink (JPA 2.0)</name>
        </repository>
    </repositories>
</project>

Как показано в XML выше, мы сделали следующее:

  • Включите использование Arquillian, используя встроенный Glassfish
  • Включить использование EclipseLink
  • Установите реквизиты Derby для последующего использования, то есть для регистрации и расположения созданной базы данных.

Создайте «дело», то есть наш JPA, EJB, CDI

Конечно, мы сначала начинаем с создания кейса, чтобы потом можно было его протестировать. Я предполагаю, что вы знакомы с JPA, EJB, CDI. Следовательно, следующие очень быстрые проблески классов, использующих эту технологию.

JPA класс, Outlet.java

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
package id.co.dwuysan.inout.entity;
 
// imports omitted
 
@Entity
@NamedQueries({
    @NamedQuery(
        name = Outlet.FIND_BY_NAME,
        query = "SELECT o FROM Outlet o WHERE o.name = :name")
})
public class Outlet implements Serializable {
 
    public static final String FIND_BY_NAME = "Outlet#FIND_BY_NAME";
 
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
 
    @Column(name = "code", length = 50, insertable = true,
            updatable = false, unique = true)
    @Size(message = "{dwuysan.nameSizeError}", min = 1, max = 50)
    @NotNull
    private String name;
 
    /* Accessors and mutators goes here */
 
    @Override
    public int hashCode() {
        // omitted
    }
 
    @Override
    public boolean equals(Object obj) {
        // omitted
    }
}

persistence.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
  <persistence-unit name="inoutPU" transaction-type="JTA">
    <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
    <jta-data-source>inoutDb</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
    </properties>
  </persistence-unit>
</persistence>

Затем давайте добавим метод продюсера для предоставления нашего PersistenceContext, а также нашего EJB, который его использует.

EntityManagerProducer.java

01
02
03
04
05
06
07
08
09
10
11
12
package id.co.dwuysan.inout.util;
 
import javax.enterprise.inject.Produces;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
 
public class EntityManagerProducer {
 
    @Produces
    @PersistenceContext
    private EntityManager em;
}

OutletService.java

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
package id.co.dwuysan.inout.service;
 
// imports omitted
 
@Stateless
@LocalBean
public class OutletService {
 
    @Inject
    private EntityManager em;
 
    @Resource
    private Validator validator;
 
    public Outlet createOutlet(final String name) {
        final Outlet outlet = new Outlet();
        outlet.setName(name);
        final Set<ConstraintViolation<Outlet>> violations = this.validator.validate(outlet);
        if (!violations.isEmpty()) { throw new ConstraintViolationException(new HashSet<ConstraintViolation<?>>(violations)); }
        return this.em.merge(outlet);
    }
 
    public Outlet getOutlet(final String name) {
        final Query query = this.em.createNamedQuery(Outlet.FIND_BY_NAME);
        query.setParameter("name", name);
        try {
            return (Outlet) query.getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }
}

Устанавливает beans.xml и ValidationMessages.properties

Не забудьте:

      • добавить beans.xml в src / main / resources / META-INF и
      • добавить ValidationMessages.properties в src / main / resources и
      • настройте ваше сообщение dwuysan.nameSizeError= error message you like here

Настроить для целей тестирования

На данный момент, если вы развернете, это должно работать. ОДНАКО, это не наша цель. Мы бы хотели, чтобы он работал под Arquillian, используя встроенный Glassfish.

Во-первых, давайте подготовим конфигурацию для встроенного Glassfish, используя базу данных Derby. Это файл glassfish-resources.xml . В моем случае я просто помещаю этот файл в новый каталог, в основном для разделения, т.е. src/test/resources-glassfish-embedded/glassfish-resources.xml .

GlassFish-resources.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE resources PUBLIC
    "-//GlassFish.org//DTD GlassFish Application Server 3.1 Resource Definitions//EN"
<resources>
    <jdbc-resource pool-name="ArquillianEmbeddedDerbyPool"
                   jndi-name="jdbc/arquillian"/>
    <jdbc-connection-pool name="ArquillianEmbeddedDerbyPool"
                          res-type="javax.sql.DataSource"
                          datasource-classname="org.apache.derby.jdbc.EmbeddedDataSource"
                          is-isolation-level-guaranteed="false">
        <property name="databaseName" value="target/databases/derby"/>
        <property name="createDatabase" value="create"/>
    </jdbc-connection-pool>
</resources>

Это довольно очевидно. Просто не забудьте настроить базу данных, которая будет создаваться в target/databases/derby чтобы при выполнении mvn clean она очищалась.

Следующий шаг — настроить Arquillian для «распознавания» этого glassfish-resources.xml . Для этого добавьте arquillian.xml src/test/resources .

GlassFish-resources.xml

01
02
03
04
05
06
07
08
09
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>
            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <engine>
        <property name="deploymentExportPath">target/arquillian</property>
    </engine>
    <container default="true" qualifier="glassfish">
        <configuration>
            <property name="resourcesXml">src/test/resources-glassfish-embedded/glassfish-resources.xml</property>
        </configuration>
    </container>
</arquillian>

Следующим шагом является подготовка нашего persistence.xml . У нас уже есть такой, но помните, что нам нужно предоставить тот, который находится в памяти, и использовать соединение jdbc, предоставляемое нашим встроенным Glassfish (см. Glassfish glassfish-resources.xml выше, которое предоставляет jdbc-resource-pool под Имя JNDI jdbc/arquillian . В моем случае я назвал этот test-persistence.xml jdbc/arquillian src/test/resources

Тест-persistence.xml

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
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    <persistence-unit name="inoutPU" transaction-type="JTA">
        <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
        <jta-data-source>jdbc/arquillian</jta-data-source>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
        <shared-cache-mode>ALL</shared-cache-mode>
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:derby:target/databases/derby;create=true" />
            <property name="eclipselink.ddl-generation" value="drop-and-create-tables" />
            <property name="eclipselink.target-database" value="Derby"/>
            <property name="eclipselink.ddl-generation" value="drop-and-create-tables"/>
            <property name="eclipselink.debug" value="OFF"/>
            <property name="eclipselink.weaving" value="static"/>
            <!--<property name="eclipselink.logging.level" value="FINEST"/>-->
            <property name="eclipselink.logging.level.sql" value="FINE"/>
            <property name="eclipselink.logging.parameters" value="true"/>
            <!--<property name="eclipselink.logging.level.cache" value="FINEST"/>-->
            <property name="eclipselink.logging.logger" value="DefaultLogger"/>
        </properties>
    </persistence-unit>
</persistence>

Когда все будет готово, мы готовы написать наш модульный тест с Arquillian. В этом случае лучше всего протестировать наш сервисный EJB, поскольку именно там мы будем использовать JPA, CDI и валидацию.

OutletServiceTest.java

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
package id.co.dwuysan.inout.service;
 
// imports omitted
 
@RunWith(Arquillian.class)
public class OutletServiceTest {   
 
    @Inject
    private OutletService outletService;
 
    @Deployment
    public static JavaArchive createTestArchive() {
        return ShrinkWrap.create(JavaArchive.class)
                .addClass(Outlet.class)
                .addClass(OutletService.class)
                .addClass(EntityManagerProducer.class)
                .addAsManifestResource("test-persistence.xml",
                    ArchivePaths.create("persistence.xml"))
                .addAsManifestResource("META-INF/beans.xml",
                    ArchivePaths.create("beans.xml"))
                .addAsResource("ValidationMessages.properties");
    }
 
    @Test
    public void testCreateOutlet() throws Exception {
        final String outletName = "Outlet 001";
        final Outlet outlet = this.outletService.createOutlet(outletName);
        Assert.assertNotNull(outlet);
 
        // check retrieval
        final Outlet retrievedOutlet = this.outletService.getOutlet(outletName);
        Assert.assertEquals(outlet.getName(), retrievedOutlet.getName());
    }
 
    @Test(expected = ConstraintViolationException.class)
    public void testCreateOutletWithEmptyName() throws Exception {
        try {
            final Outlet outlet = this.outletService.createOutlet("");
        } catch (EJBException e) {            
            final ConstraintViolationException cve = (ConstraintViolationException) e.getCause();
 
            Assert.assertEquals("Total error message should only be one",
                1, cve.getConstraintViolations().size());           
            Assert.assertEquals("Message must be correct",
                "Name must be provided",
                cve.getConstraintViolations().iterator().next().getMessage());
            throw cve;
        }
    }
}

В приведенном выше примере первый тест проверяет успешный случай. При заданном имени поиск должен привести к возврату объекта Outlet с тем же именем, что и у параметра. Хотя под поверхностью, если мы OutletService.java на тело OutletService.java , мы фактически тестируем:

      • Постоянство (JPA), в базовый дерби
      • EJB вводится в этом тесте
      • PersistenceContext внедряется через метод Producer (CDI)
      • Тестирование без проверки нарушено
      • Тестирование нашего NamedQuery

Второй тест предназначен для проверки правильности интерполирования сообщения. Ссылаясь на то, что упоминалось ранее, для моего сообщения об ошибке я поместил следующую запись в мои ValidationMessages.properties :

1
dwuysan.nameSizeError=Name must be provided

Итак, нам нужно проверить, правильно ли интерполировано сообщение от Bean Validation в Outlet .

Пожалуйста, обратите внимание на второй тест. Обратите внимание, что во-первых, мы EJBException . Это связано с тем, что любое исключение во время выполнения, EJBException внутри EJB, будет заключено в EJBException , поэтому необходимо извлечь его через #getCause() .

Итак, поехали. Теперь вы можете добавить дополнительные сервисы и начать тест Arquillian. Удачного кодирования

Будущее расследование

Конечно, многие приложения Java EE требуют аутентификации и авторизации, что обычно делается через JAAS. Например, используя мой простой пример выше, предполагалось, что служба должна быть модифицирована для получения точек доступа, к которым у текущего пользователя есть доступ, тогда, конечно, нам нужно получить личность текущего пользователя. Обычно это делается с помощью EJBContext.getCallerPrincipal() . I wonder how we can do this using Arquillian and embedded Glassfish. EJBContext.getCallerPrincipal() . I wonder how we can do this using Arquillian and embedded Glassfish.