вторник, 15 июля 2008 г.

Тестируем Spring-приложение с помощью JUnit


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

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


Для решения данной проблемы в Spring Testing появился класс SpringJUnit4ClassRunner, который и выполняет тестовые методы, попутно производя загрузку зависимостей и прочие разные вкусности, правда работает все это начиная с JUnit 4.4. Подключается класс к тестовой системе с помощью аннотации @RunWith, контекст приложения (xml-файлы с описаниями бинов) подключается к тесту с помощью аннотации @ContextConfiguration. Простой тест может выглядеть следующим образом:

package ru.vposte.test;



import junit.framework.TestCase;



import org.junit.runner.RunWith;

import org.springframework.test.context.ContextConfiguration;

import org.springframework.test.context.TestExecutionListeners;

import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

import org.springframework.test.context.support.DirtiesContextTestExecutionListener;



@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations={"classpath:applicationContext.xml"})

public class DemoSpringTestCase extends TestCase {



    @Autowired

    ISimpleBean mybean;



    @Test  

    public void testInjectBlogDao() throws Exception {

    System.out.println("BlogDAO = " + mybean);

    assertNotNull(mybean);       

    }

}

 


Для инъекции зависимостей используется автовайринг по типу (аннотация @Autowired). Также следует иметь ввиду, что в JUnit4 поведение тестовых классов отличается от JUnit3. Это касается двух моментов:


  • методы setUp() и tearDown() НЕ ВЫЗЫВАЮТСЯ до начала и после окончания выполнения тестового метода,

  • каждый тестирующий метод должен быть отмечен аннотацией @Test. Методы, не отмеченные аннотацией @Test не вызываются



Вместо перегрузки методов setUp() и tearDown() в случае SpringJUnit4ClassRunner используется механизм слушателей (listeners). Каждый слушатель - это класс, реализующий интерфейс TestExecutionListener, в котором объявлены следующие методы:

void prepareTestInstance(TestContext testContext) throws Exception;

вызывается при создании экземпляра тестирующего класса.

void beforeTestMethod(TestContext testContext) throws Exception;

вызывается ДО выполнения тестового метода

public void afterTestMethod(TestContext testContext) throws Exception;

вызывается ПОСЛЕ выполнения тестового метода

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

Регистрируются слушатели с помощью аннотации @TestExecutionListeners. Для осуществления инъекции зависимостей в тестирующий класс служит DependencyInjectionTestExecutionListener, также существуют слушатели TransactionalTestExecutionListener - для управления транзакциями при выполнении методов, отмеченных аннотациями @Transactional и @NotTransactional, а также DirtiesContextTestExecutionListener - для управления так называемой "грязной" загрузкой контекста.

Давайте для примера напишем слушатель, который будет регистрировать хибернейтовскую сессию, предотвращая ее удаление после выполнения операции и тем самым позволяя работать с Lazy-загрузкой данных. Проблема в том, что созданные Spring'ом HibernateSession закрываются после выполнения операции над данными, соответственно при последующим обращении к лениво-загружаемым данным (например к коллекции адресов некой персоны) нам скажут, что сессия закрыта и выбросят замечательный эксепшн. Чтобы этого не произошло необходимо явно создать сессию и зарегистрировать ее в TransactionSynchronizationManager. После этого для всех операций с данными будет использоваться именно эта сессия и естественно никаких проблем с Lazy-загружаемыми данными не будет.

Сразу скажу, что Spring предоставляет абстрактный класс AbstractTestExecutionListener, который реализует пустые методы интерфейса TestExecutionListener. От этого класса мы и будем наследоваться. Код нашего листенера может быть следующим:

package ru.vposte.test;



import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.springframework.orm.hibernate3.SessionFactoryUtils;

import org.springframework.orm.hibernate3.SessionHolder;

import org.springframework.test.context.TestContext;

import org.springframework.test.context.support.AbstractTestExecutionListener;

import org.springframework.transaction.support.TransactionSynchronizationManager;



public class HibernateSessionRegistrationTestExecutionListener extends AbstractTestExecutionListener {

   

    private static final String SESSION_FACTORY_BEAN = "sessionFactory";

   

    private SessionFactory _sessionFactory;

   

    private Session _session;  

   

    @Override

    public void beforeTestMethod(TestContext testContext) throws Exception {

        _sessionFactory = (SessionFactory) testContext.getApplicationContext()

                .getBean(SESSION_FACTORY_BEAN);

        _session = SessionFactoryUtils.getSession(_sessionFactory, true);

        TransactionSynchronizationManager.bindResource(_sessionFactory,

                new SessionHolder(_session));

    }

   

    @Override

    public void afterTestMethod(TestContext testContext) throws Exception {

        TransactionSynchronizationManager.unbindResource(_sessionFactory);

        SessionFactoryUtils.releaseSession(_session, _sessionFactory);

    }

}

 


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

Подключить листенеры к тестирующему классу можно например так:

@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations={"classpath:applicationContext.xml"})

@TestExecutionListeners({

    DependencyInjectionTestExecutionListener.class,

    DirtiesContextTestExecutionListener.class,

    HibernateSessionRegistrationTestExecutionListener.class

})

public class DemoSpringTestCase extends TestCase {

 


Как видим нет никаких сложностей в тестировании спринг-приложений, а уж сколько времени оно экономит при разработке, особенно относительно низкоуровневых вещей (бизнес-логики, ДАО и т.д.), я говорить не буду. Более того, тестирование позволяет разрабатывать работающие низкоуровневые части когда интерфейс пользователя еще не готов, а если учесть всю мощь мок-объектов, то позволяет производить разрозненую разработку частей приложения.

Безбажных вам проектов.

Понравилось сообщение - подпишись на блог

15 комментариев:

black комментирует...

Я бы не сказал, что все так хорошо. Например вы тестируете "голенький" spring контекст, а если разговор пойдет об тесте полноценного контекста для веб-приложения, то будут проблемы с tiles (бо, он гад пытается регистрироваться в sessioncontext-е, которой просто нет). будут проблемы с загрузкой messageboundles т.к. пути отсчитываются для веб относительно корня веб-приложения /WEB-INF/bla-bla, а при запуске теста от classpath всего этого нет.

И красивого способа, кроме создания пары наборов spring xml-ек, я не вижу, а вы?

Samolisov Pavel комментирует...

Данная статья является скорее вводной и в ней затрагиваются в основном проблемы тестирования ДАО, бизнес-логики и прочих неинтерфейсных частей. Понятно, что когда мы начинаем работать с WEB все становится гораздо веселее. Но, думаю, со временем поделюсь опытом и в этой сфере.

Про пару наборов спринг-xml, я не вижу в этом проблемы. Добавляем в аннотацию еще один параметр и тем самым подключаем еще одну xml-ку.

samsonych комментирует...

Класс, Павел, спасибо за статью.
Сейчас как раз присматриваюсь к Spring'у. Пока больше к DAO, Acegi.
Есть желание перевести довольно-таки объемный проект на основу данного фрэймворка. Хотелось бы больше информации в этом направлении.

ЗЫ. Пользуясь моментом, пусть немного с опозданием, хочу поздравить тебя со cдачей диплома и получением статуса дипломированного специалиста. Удачи, творческих успехов тебе, продолжай в том же духе ;)

Samolisov Pavel комментирует...

Сам пока только начинаю, но спринг уже успел понравится (правда раньше я его юзал, но все по мелочи. С нуля приложение на нем не проектировал). В скором времени надеюсь еще что-нибудь написать по его поводу )

Спасибо за поздравление!

Stas Ostapenko комментирует...

А чем таким интересным будет отличаться описанный подход от наследования юнит теста от org.springframework.test.
AbstractTransactionalSpringContextTests ? Я юзал как раз второй подход.

Samolisov Pavel комментирует...

Если честно, я с указаным вами подходом особо не разбирался, мне показалось, что там не поддерживаются аннотации и вся параметризация выполняется в коде, вызовом соответствующих методов. Все же ИМХО аннотированный код более удобочитаем. Плюс удобный механизм листенеров.

Владимир Долженко комментирует...

не обязательно имя bean'а должно совпадать с именем свойства теста, которому делается autowire - в этом случае поможет аннотация @Qualifier:

@Autowired
@Qualifier("springBeanName")
private Smth myName;

Владимир Долженко комментирует...

не совсем корректно было сказано про использование @Qualifier.
@Autowire разрешает зависимости по типу объекта, а не по имени. В случае, если имя bean'а отличается от имени свойства, но bean такого типа один - то нет потребности в использовании @Qualifier. Если же bean такого типа несколько, то @Autowire спотыкается не зная кого выбрать - именно для этого и нужен @Qualifier.

Дальше лучше - если использовать типизированные коллекции или объекты, то autowire не может разрешить зависимости в виде параметризованных классов. Рекомендуется использовать @Resource из JS250 вместо @Autowire.

подробнее: http://jira.springframework.org/browse/SPR-3946

Анонимный комментирует...

Комментарии по поводу setUp() и tearDown(), если позволите :)

Они не вызываются не из-за использования SpringJUnit4ClassRunner, а потому что используется JUnit4 вместо JUnit3. В JUnit4 тестовый класс можно вообще ни от чего не наследовать - достаточно использовать аннотации.

Вместо setUp можно написать так

@Before
public void beforeTest(){
..
}

Вместо tearDown() - @After

Samolisov Pavel комментирует...

Господа, большое спасибо за ваши комментарии, они будут полезны как мне так и читателям блога.

З.Ы. Внес изменения в текст статьи по поводу setUp и tearDown

DiMaN комментирует...

public class DemoSpringTestCase extends TestCase {

@Autowired
ISimpleBean mybean;

@Test
public void testInjectBlogDao() throws Exception {
System.out.println("BlogDAO = " + _blogDao);
assertNotNull(_blogDao);
}
}

Что за "_blogDao"? Autowired ведь "mybean"?

Samolisov Pavel комментирует...

Большое спасибо за ваше замечание - поправил эту досадную опечатку.

Анонимный комментирует...

на счет void prepareTestInstance(TestContext testContext) throws Exception;

здесь стоит еще добавить о наличии аннотаций @BeforeClass, @AfterClass

Keip комментирует...

привет!

не подскажете, как в maven добавить dependency:

org.springframework.test

какой репозиторий использовать, чтобы пакет скачался этот?

Pavel Samolisov комментирует...

Здравствуйте. К сожалению не подскажу, т.к. с maven почти не работал.

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!