Тестирование Spring приложений. Транзакции в тестировании

    spring-overview

    Про полезность подхода TDD (разработка через тестирование, test driven development) не слышал только ленивый или глухой. Но сегодня мы не будем обсуждать всю его полезность и красоту, а также проблемы и недостатки. Сегодня мы попробуем посмотреть, как разрабатывать unit-тесты для spring приложений. Также мы немного тронем ручное управление транзакциями в unit-тестах.

    Небольшое замечание: иногда тесты spring приложений это не совсем unit-тесты, потому что мы можем в них поднять и задействовать очень сложное окружение (БД, WebService и так далее). Подобные тесты это скорее интеграционные тесты, но я думаю что сейчас философские вопросы мы поднимать не будем.

    Для начала предлагаю согласовать некоторые термины и понятия:
    • Unit-тест — тест, который проверяет поведение небольшой части приложения. Эта часть может быть одним классом, одним методом или набором классов, который реализуют какое-то архитектурное решение, и это решение необходимо проверить на работоспособность. За подробностями обращайтесь сюда или туда.
    • Application context config — конфигурационный файл в xml формате для описания структуры spring приложения. Про spring читаем тут или там.
    • DAO — объект доступа к данным или data acess object. Основное предназначение этого шаблона проектирования: связать вместе БД и наше приложение. За подробностями идем сюда или туда.
    • Транзакция — группа последовательных операций, которая представляет собой логическую единицу работы с данными. Транзакция может быть выполнена либо целиком и успешно, соблюдая целостность данных и независимо от параллельно идущих других транзакций, либо не выполнена вообще и тогда она не должна произвести никакого эффекта. Про транзакции читаем тут или там.

    Все остальные термины и понятия стандартны и давно устоялись или их описание тут некритично. Например, такое понятие как IoC (инверсия управления, inversion of control).

    Совсем забыл, про написание unit-тестов для spring-приложений я пишу не первый, немного есть тут и там.

    Итак, у нас стоит цель: протестировать поведение класса в spring-приложении, дополнительно необходимо вручную управлять транзакциями. Для этого мы создадим простое spring-приложение и напишем unit-тест. Наш unit-тест при запуске будет инициализировать application context config нашего spring приложения и после этого вызывать методы у тестируемого нами класса. Также мы разработаем отдельный тест, в котором будем управлять транзакциями вручную.

    Для начала создадим приложение, которое будем тестировать. Технологии в приложении будут следующие:
    1. Средство сборки и компиляции — Apache Maven.
    2. База данных — HSQLDB.
    3. Средство для отображения классов в базу данных (Object relation mapping) — Hibernate.
    4. Средство для конфигурирования приложения — Spring framework.
    5. Средство для создания unit-тестов — JUnit.

    Конечная структура файлов в приложении будет выглядеть следующим образом:structure
    Кстати вы можете загрузить себе (из SVN) и посмотреть в браузере исходные коды работающего приложения.

    Начнем?

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

    Во вторых, мы создадим java persistente entity класс — ru.intr13.example.springTransactionalTest.Data. Данный класс у нас будет описывать модель данных и при помощи библиотеки hibernate он будет отображаться в БД (будет автоматически создана таблица в базе данных). Также на этом шаге мы создадим файл hibernate.cfg.xml, где сделаем ссылку на разработанный нами класс.

    В третьих, мы создадим интерфейс — ru.intr13.example.springTransactionalTest.DataDao. В котором опишем основные методы для работы с нашей БД. Методы checkpoint и shutdown предназначены для работы с hsqldb и их наличие связано с особенностью работы данной БД (подробности тут).

    В четвертых, мы создадим реализацию разработанного нами интерфейса — ru.intr13.example.springTransactionalTest.DataHibernateDao. Где реализуем все методы описанные в интерфейсе DataDao. Стоит отметить, что данный класс наследуется от класса org.springframework.orm.hibernate3.support.HibernateDaoSupport, который в свою очередь является часть библиотеки Spring Dao и в нем уже реализованы методы для удобной работы с БД.

    В пятых, мы создадим application context config файл для конфигурирования нашего приложения. В котором мы опишем:
    • Источник данных (dataSource), в котором мы опишем параметры подключения к нашей hsqldb базе данных.
    • Фабрику для работы с подключениями к базе данных и для отображения нашей модели в БД (sessionFactory). При конфигурировании фабрики мы укажем ссылку на файл hibernate.cfg.xml, где описаны все классы нашей модели. Также мы пропишем параметры создания и работы с базой данных, ссылку на источник данных.
    • Разработанный нами сервис (dataDao). При конфигурировании мы укажем ссылку на sessionFactory.
    • Менеджер транзакций (transactionManager). При конфигурировании которого мы укажем ссылку на sessionFactory и также укажем: на какие методы нам надо начинать новую транзакцию.

    В шестых, создадим тестовое приложение, которое проинициализирует application context config и немного поработает с разработанным нами сервисом. Результаты работы сохраняться в нашу локальную БД, что можно наблюдать в файле data/test.db.script (данный файл содержит данные нашей БД):
    CREATE SCHEMA PUBLIC AUTHORIZATION DBA
    CREATE MEMORY TABLE DATA(ID BIGINT NOT NULL PRIMARY KEY,TEXT VARCHAR(255))
    CREATE MEMORY TABLE HIBERNATE_SEQUENCES(SEQUENCE_NAME VARCHAR(255),SEQUENCE_NEXT_HI_VALUE INTEGER)
    CREATE USER SA PASSWORD ""
    GRANT DBA TO SA
    SET WRITE_DELAY 10
    SET SCHEMA PUBLIC
    INSERT INTO DATA VALUES(1,'one')
    INSERT INTO DATA VALUES(2,'two')
    INSERT INTO DATA VALUES(3,'three')
    INSERT INTO HIBERNATE_SEQUENCES VALUES('Data',1)

    Итак, тестовое приложение создано и теперь надо разработать unit-тест. Для этого мы создаем класс ru.intr13.example.springTransactionalTest.DataDaoTest. Данный класс является наследником класса org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests, что позволяет при запуске тестов поднимать application context config. Делается это при помощи аннотации для класса-теста:
    @ContextConfiguration(locations = { "/applicationContext.xml" }).

    Но для полноценного тестирования нам требуется чтобы у нашего теста была возможность получить разработанный нами сервис. Для этого мы прописываем в нашем тесте поле со ссылкой на сервис и ставим для этого поля аннотацию — @Autowired:
    @Autowired
    private DataDao dataDao;

    Теперь при запуске тестов в данное поле будет установлена ссылка на разработанный ранее сервис.

    И в конце остается написать текст unit-теста:
    @Test
    public void simpleTest() {
      String text = UUID.randomUUID().toString();
      dataDao.save(new Data(text));
      Collection result = dataDao.find(text);
      Assert.assertEquals(1, result.size());
      Assert.assertEquals(text, result.iterator().next().getText());
    }

    Данный тест создает объект Data, сохраняет его в БД, и потом ищет объект Data по содержимому.

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

    Для ручного управления транзакциями в тестах, нам нужно получить описанный в application context config менеджер транзакций (transactionManager), что делается через создание поля в нашем тесте:
    @Autowired
    private PlatformTransactionManager transactionManager;

    Далее мы просто создаем новую транзакцию:
    TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW));

    и потом делаем для нее commit (сохранение):
    transactionManager.commit(transaction);

    или rollback (откат):
    transactionManager.rollback(transaction);

    Зная все выше приведенное, мы можем написать следующий тест, который создает несколько транзакций вручную:
    @Test
    public void comlexTest() {
      TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW));
      String text = UUID.randomUUID().toString();
      dataDao.save(new Data(text));
      transactionManager.commit(transaction);
      transaction = transactionManager.getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRES_NEW));
      Collection result = dataDao.find(text);
      Assert.assertEquals(1, result.size());
      Assert.assertEquals(text, result.iterator().next().getText());
      transactionManager.rollback(transaction);
    }

    Конечный вариант теста можно посмотреть здесь.

    Итого: мы создали тестовое приложение на базе spring framework (исходные коды лежат здесь). Был продемонстрирован способ тестирования отдельных сервисов в spring framework. Также был показан способ ручного управления транзакциями в unit-тестах. В результате мы увидели что создание простых unit-тестов для spring framework довольно простая задача. Вопрос о целесообразности и необходимости разработки подобных тестов рассмотрен не был, это тема для отдельной беседы.

    p/s
    Это отредактированная версия поста из моего личного блога. Не сочтите за рекламу:)

    p/s/s
    Картинка найдена здесь. Кстати внимательный человек заметит одну забавную вещь:)
    Share post

    Similar posts

    Comments 45

    • UFO just landed and posted this here
        –2
        Каким образом Вы создаете тесты при помощи аннотаций? Наследование от тестовых классов на сколько я помню единственный вариант ИМХО.

        Кстати аналогично можно в TestNG транзкациями управлять, только там транзакция начинается перед а
        заканчивается после теста.
        • UFO just landed and posted this here
            –1
            Вариант хороший. С Junit давно работал и без аннотаций. *Пошел перечитывать доку* ))
            • UFO just landed and posted this here
                +1
                Главное не переборщить)). Был случай, когда в bean'ах были аннотации от coherence, json. Код становился просто трудночитаемым, что не очень то и хорошо.
                  0
                  В аннтациях минус в том, что конфигурация по сути расползает по всему коду. Удобны, без вариантов. Но такой моментик есть.
                  • UFO just landed and posted this here
                    • UFO just landed and posted this here
                        0
                        Не понял.
                        • UFO just landed and posted this here
                            0
                            Хорошая это какая из трех?
                            • UFO just landed and posted this here
                                0
                                А где она показывает в одном месте все Hibernate аннотации?
                                • UFO just landed and posted this here
                                    0
                                    Причем тут я, мне интересно о чем ВЫ говорите. Просто вдруг я какую то клевую фишку IDEA упускаю.
                                    • UFO just landed and posted this here
                                        0
                                        Вот те раз, в итоге выходит я еще и не в контексте. Ок, простой пример. Если будем схему маппинга делать через аннотации @Entity, @Column и т.п. то сложно будет увидеть даже банально из коммитов когда изменили маппинг, меняя код, а когда не меняли. Надо каждый файл посмотреть diff. Если же писать самому xml, то правка маппинга отчетливо видна — изменение в xml, ожидай изменения в маппинге. Вот и выходит, что конфигурация расползается по всему коду.
                                        • UFO just landed and posted this here
                                            0
                                            Об этом и говорят, что приходится кроме анализа самих изменений маппанга еще и искать ГДЕ они произошли. В случае пары BlaBlaBlaEntity это не сложно, а когда за неделю команда наменяла в коде горы, вы предлагаете просматривать каждый Entity? В случае xml просто делается diff двух ревизий, а в случае аннотаций?
                0
                Тут тоже транзакция начинает при начале каждого теста и заканчивается при окончании теста :)
                Только иногда требуется несколько транзакций в тесте:)
                +1
                1. Наследование в тестах сложилось исторически :) Возможно в следующем проекте вместо наследования будет аннотация RunWith junit.org/apidocs/org/junit/runner/RunWith.html
                2. Конфигурационный файл для hibernate нужен для JBossTools и liquibase :)
                • UFO just landed and posted this here
                    +2
                    Под словом исторически имеется ввиду, что проект был начат без меня:) Я потихонечку его мигрирую в правильную сторону, но тут не все так гладко:)

                    p/s
                    Кому ночь, а кому на работе быть через полтора часа:)

                    • UFO just landed and posted this here
                +1
                давно пользуюсь примерно аналогичным окружением. только вместо голого junit, использую связку testng+unitils. позволяет разбивать тесты на группы, и солидно так сокращает код, за счет использования ReflectionAssert и прочих приятностей из unitils.
                  +1
                  Спасибо за Unitils! ReflectionAssert, мне очень не хватало.
                    +1
                    не за что. еще что хотел сказать по поводу этой статьи — интеграционные тесты со спрингом конечно бывают нужны, но по сути в большинстве случаев лучше изолировать тестируемый код, подставляя вместо зависимостей Mock'и. Которые тоже здорово и просто делать с помощью Unitils. То есть, к примеру, для тестирования сервисов удобно создать mock'и dao и проверять не «во всю глубину», а только контракт взаимодействия сервиса и dao.
                  0
                  а почему спринг не третий?
                    +1
                    Причина проста: Spring 3.0.0 Milestone 4
                    0
                    А как вы думаете — писать такие тесты стоит на уровне DAO или Services? Или и там и там?
                      0
                      У нас DAO классы обычно содержат код для получения данных из БД и простейшего преобразования их. Например вернуть map или set по результатам выполнения запросов. Поэтому их тестировать особого смысла нет, там слишком простой код. Хотя иногда тестирование сложных select или update запросов требуется.

                      В свою очередь services обычно содержит сложную логику преобразования одно объектной модели в другую (обработка и расчет исходных данных). Поэтому их тестировать надо, и лучше всего это делать так как уже говорил выше victorb (http://habrahabr.ru/blogs/java/70168/#comment_2003211). То есть создавая объекты заглушки для остального окружения. Например, при тестировании сервиса считается что остальное окружение работает предсказуемо и в нем нет ошибок о которых мы не знаем. Для создания объектов-заглушек мы используем EasyMock.

                      А тесты описанные в посте требуются для оценки работоспособности: Services -> DAO -> БД. Они интеграционные и их обычно не так много как обычных юнит-тестов.

                      p/s
                      Самое главное без фанатизма, разработка некоторых тестов нерациональна и алогична.
                        0
                        Я собственно почему спрашиваю. Был недавно проект — Java на стороне сервера и Flex в качестве клиента.
                        Схема доступа к данным следующая: DAOServicesDTO Assemblers Facade.
                        Опыт показывает, что писать тесты на DAO в этом случае — совершенно бесполезно. Т.е. пока не дернуть соответствующий метод фасада — нет никакой гарантии что это будет работать.
                        И моки на объекты оказались скорее вредны, чем полезны
                        И действительное положение вещей показывали именно функциональные тесты на фасады
                        В общем какой-то диссонанс полный с теорией…
                          0
                          Ну не скажите, порой без юнит-тестов очень сложно жить. Для получения пользы от них надо расслаивать систему (повышать связность и понижать сцепление). И тогда будет вам счастье, когда у вас есть отдельные части системы, которые легко тестировать, тогда очень сильно возрастает уверенность в коде.

                          По поводу вашего случая: скорее всего у вас на сервере был только интерфейс для доступа данных, вся бизнес-логика была на flex-клиенте (возможно у вас даже было смешение бизнес-логики и представления данных). Если так, то тогда действительно вам юнит тесты особо не требуются, хотя может быть стоит тестировать flex-приложение? Также остается открытым вопрос об объеме передаваемых по сети данных и сложности поддержки flex-приложения:)

                          Кстати, а можно поподробнее про архитектуру вашего приложения, просто любопытно:)
                            0
                            Да архитектура в принципе простая, как всегда.
                            1. DAO (Hibernate)
                            2. Services
                            3. DTO assemblers (жуткий самописный фрамеворк на аннотациях)
                            4. Facade

                            Бизнес логики на клиенте не было. Транзакции висели на фасадах.
                            Самым проблемным был п.3. Было необходимо поддерживать довольно сложные правила конверсии из бизнес-сущностей в DTO и обратно (в том смысле что конверсии одного и того же бина с-клиента и на-клиент чаще всего отличались). Собственно, именно из-за этого я разочаровался во Flex — кажущаяся красивость и простота оборачивается в DTO-hell.

                            Вот и получилось собственно, что для того, чтобы хоть что-то протестировать надо писать integration тесты для фасадов на реальную базу данных.

                              0
                              Помнится был у меня один проект, где приходилось модель данных преобразовывать в модель данных вебсервиса (AXIS). Так вот, я там писал тесты по преобразованию AXIS модели в обычную модель. Тут наверное тоже можно было попробовать написать тесты для правил конверсии. Причем эти тесты очень хорошо дополнили бы интеграционные тесты:)
                                0
                                Тесты для правил конверсии — да, это хорошо. Но как быть с тестами на правильное использование правил конверсии?
                                  0
                                  Использование правил конверсии наверное в Fasade. Поэтому если использование правил конверсии сложная штука, то заменяем дао на моки:)
                                  Тут ведь главное без фанатизма, чтобы не было тестов ради тестов:)
                      0
                      Интересно, а почему 8 минусов за пост? Что не так? Очень бы хотелось негативный отзыв:)
                        –1
                        Зачем вы так подробно все описываете где посмотреть? Те, кого интересует тестирование в Spring приложениях подавно знаю что такое Hiberante & Maven & JUnit.
                          0
                          Всегда есть кто то, кто все знает. Но не всегда тот кто знает, тот понимает.

                          Итого: (все) больше чем (знающие) больше чем (понимающие).
                            0
                            Вроде по делу сказал, а вы тут высокими словами. Сомнительно что кто то рванет изучать всю эту цепочку исключительно чтобы понять статью. Ну и для наглядности прикольней транзакциями рулит через аннтоации. Ну да не надо в пики все воспринимать.
                              0
                              Это же просто ссылки, по ним можно и не ходить:)

                              Согласен, аннотациями удобнее. Но стояла задача: в одном тесте запускать несколько транзакций. Возможно это можно сделать вызывая в тесте два метода, у каждого из которых аннотация @Tranactional, но у меня это почему то не заработало. Так оказалось проще.
                            0
                            Но все равно спасибо за почти негативный отзыв. Возможно действительно, чересчур подробно все описано:)
                            0
                            Спасибо за статью.

                            В вебприложениях DataSource для соединения с БД обычно попадает в applicationConext.xml по средством JNDI, например из META-INF/context.xml для томката.

                            Поэтому было бы интересно написание подобного теста на базе настоящего applicationConext.xml которому просто подсовывается тестовый DataSource через JNDI.

                            Only users with full accounts can post comments. Log in, please.