Статьи

Факты гибернации: стратегии тестирования интеграции

Мне нравится Integration Testing, это хороший способ проверить, какие SQL-запросы Hibernate создает за кулисами. Но для интеграционных тестов требуется работающий сервер базы данных, и это первый выбор, который вам нужно сделать.

1. Использование производственного локального сервера базы данных для тестирования интеграции

В производственной среде я всегда предпочитаю использовать инкрементные сценарии DDL, так как я всегда могу знать, какая версия развернута на данном сервере и какие новые сценарии требуются для развертывания. Я полагаюсь на Flyway, чтобы управлять обновлениями схемы для меня, и я очень доволен этим.

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

Главный недостаток — скорость испытаний. Использование внешней базы данных подразумевает дополнительные временные затраты, которые могут легко выйти из-под контроля в большом проекте. В конце концов, кто любит проводить ежедневные 60-минутные тестовые упражнения?

2. Интеграционное тестирование базы данных в памяти

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

Есть много баз данных в памяти, которые вы можете выбрать: HSQLDB , H2 , Apache Derby и многие другие.

Я использовал две стратегии генерации схем в памяти, каждая из которых имеет свои плюсы и минусы, которые я собираюсь объяснить следующим образом.

2.1 Использование hibernate.hbm2ddl.auto = ”update”

Hibernate очень гибок в настройке . К счастью, мы можем настроить генерацию DDL с помощью свойства SessionFactory «hibernate.hbm2ddl.auto».

Самый простой способ развернуть схему — использовать опцию «обновить». Это полезно для тестирования. Я бы не стал полагаться на это в производственной среде, для которой инкрементные сценарии DDL являются лучшим подходом.

Таким образом, выбор опции «обновить» является одним из вариантов управления схемой Integration Testing.

Вот как я использовал это в примерах кода Hibernate Facts .

Начнем с конфигурации JPA, которую вы можете найти в файле META-INF / 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
<?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="testPersistenceUnit" transaction-type="JTA">
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <exclude-unlisted-classes>false</exclude-unlisted-classes>
 
        <properties>
            <property name="hibernate.archive.autodetection"
                      value="class, hbm"/>
            <property name="hibernate.transaction.jta.platform"
                      value="org.hibernate.service.jta.platform.internal.BitronixJtaPlatform" />
            <property name="hibernate.dialect"
                      value="org.hibernate.dialect.HSQLDialect"/>
            <em><property name="hibernate.hbm2ddl.auto"
                      value="update"/></em>
            <property name="hibernate.show_sql"
                      value="true"/>
        </properties>
    </persistence-unit>
</persistence>

И конфигурация источника данных выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
<bean id="dataSource" class="org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy">
        <constructor-arg>
            <bean class="bitronix.tm.resource.jdbc.PoolingDataSource" init-method="init"
                  destroy-method="close">
                <property name="className" value="bitronix.tm.resource.jdbc.lrc.LrcXADataSource"/>
                <property name="uniqueName" value="testDataSource"/>
                <property name="minPoolSize" value="0"/>
                <property name="maxPoolSize" value="5"/>
                <property name="allowLocalTransactions" value="true" />
                <property name="driverProperties">
                    <props>
                        <prop key="user">${jdbc.username}</prop>
                        <prop key="password">${jdbc.password}</prop>
                        <prop key="url">${jdbc.url}</prop>
                        <prop key="driverClassName">${jdbc.driverClassName}</prop>
                    </props>
                </property>
            </bean>
        </constructor-arg>
    </bean>

Я думаю, что Bitronix — один из самых надежных инструментов, с которыми я когда-либо работал. Когда я разрабатывал приложения JEE, я использовал диспетчер транзакций, предоставляемый используемым сервером приложений. Для проектов на базе Spring мне пришлось использовать автономный менеджер транзакций, и после оценки JOTM, Atomikos и Bitronix я остановился на Bitronix. Это было 5 лет назад, и с тех пор я развернул несколько приложений, используя их.

Я предпочитаю использовать транзакции XA, даже если в настоящее время приложение использует только один источник данных. Мне не нужно беспокоиться о каких-либо заметных потерях производительности при использовании JTA , поскольку Bitronix использует 1PC (однофазное принятие), когда текущая заявка использует только один зачисленный источник данных. Это также позволяет добавлять до одного источника данных не-XA благодаря оптимизации Last Resource Commit .

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

К сожалению, как бы ни был прост этот метод генерации DDL, у него есть один недостаток, который мне не слишком нравится. Я не могу отключить параметр allowLocalTransactions, так как Hibernate создает сценарий DDL и обновляет его вне транзакции XA.

Другим недостатком является то, что вы слабо контролируете, какой сценарий DDL Hibernate развертывает от вашего имени, и в этом конкретном контексте я не люблю ставить под угрозу гибкость из-за удобства.

Если вы не используете JTA и вам не нужна гибкость в определении того, какая схема DDL будет развернута на вашем текущем сервере базы данных, тогда hibernate.hbm2ddl.auto = ”update” , вероятно, является вашим правильным выбором.

2.2 Гибкая схема развертывания

Этот метод состоит из двух этапов. В первом случае Hibernate генерирует сценарии DDL, а во втором — их развертывание по индивидуальному заказу.

Для создания сценариев DDL мне нужно использовать следующую задачу Ant (даже если она запускается через Maven), и это потому, что на момент написания этой статьи не было подключаемого модуля Hibernate 4 Maven:

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
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-antrun-plugin</artifactId>
    <executions>
        <execution>
            <id>generate-test-sql-scripts</id>
            <phase>generate-test-resources</phase>
            <goals>
                <goal>run</goal>
            </goals>
            <configuration>
                <tasks>
                    <property name="maven_test_classpath" refid="maven.test.classpath"/>
                    <path id="hibernate_tools_path">
                        <pathelement path="${maven_test_classpath}"/>
                    </path>
                    <property name="hibernate_tools_classpath" refid="hibernate_tools_path"/>
                    <taskdef name="hibernatetool"
                             classname="org.hibernate.tool.ant.HibernateToolTask"/>
                    <mkdir dir="${project.build.directory}/test-classes/hsqldb"/>
                    <hibernatetool destdir="${project.build.directory}/test-classes/hsqldb">
                        <classpath refid="hibernate_tools_path"/>
                        <jpaconfiguration persistenceunit="testPersistenceUnit"
                                          propertyfile="src/test/resources/META-INF/spring/jdbc.properties"/>
                        <hbm2ddl drop="false" create="true" export="false"
                                 outputfilename="create_db.sql"
                                 delimiter=";" format="true"/>
                        <hbm2ddl drop="true" create="false" export="false"
                                 outputfilename="drop_db.sql"
                                 delimiter=";" format="true"/>
                    </hibernatetool>
                </tasks>
            </configuration>
        </execution>
    </executions>
    ...
</plugin>

Имея сценарии «создания» и «удаления» DDl, теперь нам нужно развернуть их при запуске контекста Spring, и это делается с помощью следующего пользовательского класса Utility:

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
66
67
68
69
70
71
72
73
74
75
76
public class DatabaseScriptLifecycleHandler implements InitializingBean, DisposableBean {
 
    private final Resource[] initScripts;
    private final Resource[] destroyScripts;
 
    private JdbcTemplate jdbcTemplate;
 
    @Autowired
    private TransactionTemplate transactionTemplate;
 
    private String sqlScriptEncoding = "UTF-8";
    private String commentPrefix = "--";
    private boolean continueOnError;
    private boolean ignoreFailedDrops;
 
    public DatabaseScriptLifecycleHandler(DataSource dataSource,
                                          Resource[] initScripts,
                                          Resource[] destroyScripts) {
        this.jdbcTemplate = new JdbcTemplate(dataSource);
        this.initScripts = initScripts;
        this.destroyScripts = destroyScripts;
    }
 
    public void afterPropertiesSet() throws Exception {
        initDatabase();
    }
 
    public void destroy() throws Exception {
        destroyDatabase();
    }
 
    public void initDatabase() {
        final ResourceDatabasePopulator resourceDatabasePopulator = createResourceDatabasePopulator();
        transactionTemplate.execute(new TransactionCallback<Void>() {
            @Override
            public Void doInTransaction(TransactionStatus status) {
                jdbcTemplate.execute(new ConnectionCallback<Void>() {
                    @Override
                    public Void doInConnection(Connection con) throws SQLException, DataAccessException {
                        resourceDatabasePopulator.setScripts(getInitScripts());
                        resourceDatabasePopulator.populate(con);
                        return null;
                    }
                });
                return null;
            }
        });
    }
 
    public void destroyDatabase() {
        final ResourceDatabasePopulator resourceDatabasePopulator = createResourceDatabasePopulator();
        transactionTemplate.execute(new TransactionCallback<Void>() {
            @Override
            public Void doInTransaction(TransactionStatus status) {
                jdbcTemplate.execute(new ConnectionCallback<Void>() {
                    @Override
                    public Void doInConnection(Connection con) throws SQLException, DataAccessException {
                        resourceDatabasePopulator.setScripts(getDestroyScripts());
                        resourceDatabasePopulator.populate(con);
                        return null;
                    }
                });
                return null;
            }
        });
    }
 
    protected ResourceDatabasePopulator createResourceDatabasePopulator() {
        ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
        resourceDatabasePopulator.setCommentPrefix(getCommentPrefix());
        resourceDatabasePopulator.setContinueOnError(isContinueOnError());
        resourceDatabasePopulator.setIgnoreFailedDrops(isIgnoreFailedDrops());
        resourceDatabasePopulator.setSqlScriptEncoding(getSqlScriptEncoding());
        return resourceDatabasePopulator;
    }
}

который настроен как:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
<bean id="databaseScriptLifecycleHandler" class="vladmihalcea.util.DatabaseScriptLifecycleHandler"
      depends-on="transactionManager">
    <constructor-arg name="dataSource" ref="dataSource"/>
    <constructor-arg name="initScripts">
        <array>
            <bean class="org.springframework.core.io.ClassPathResource">
                <constructor-arg value="hsqldb/create_db.sql"/>
            </bean>
        </array>
    </constructor-arg>
    <constructor-arg name="destroyScripts">
        <array>
            <bean class="org.springframework.core.io.ClassPathResource">
                <constructor-arg value="hsqldb/drop_db.sql"/>
            </bean>
        </array>
    </constructor-arg>
</bean>

На этот раз мы можем избавиться от любой локальной транзакции, чтобы мы могли безопасно установить:

1
<property name="allowLocalTransactions" value="false" />
  • Код доступен на GitHub .