В рамках разработки своей социальной сети решил освоить такую замечательную вещь как юнит-тесты. Точнее не столько освоить (с концепцией юнит-тестирования и базисом использования 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);
}
}
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, в котором объявлены следующие методы:
вызывается при создании экземпляра тестирующего класса.
вызывается ДО выполнения тестового метода
вызывается ПОСЛЕ выполнения тестового метода
Каждый из этих методов в качестве аргумента принимает 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);
}
}
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 {
@ContextConfiguration(locations={"classpath:applicationContext.xml"})
@TestExecutionListeners({
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class,
HibernateSessionRegistrationTestExecutionListener.class
})
public class DemoSpringTestCase extends TestCase {
Как видим нет никаких сложностей в тестировании спринг-приложений, а уж сколько времени оно экономит при разработке, особенно относительно низкоуровневых вещей (бизнес-логики, ДАО и т.д.), я говорить не буду. Более того, тестирование позволяет разрабатывать работающие низкоуровневые части когда интерфейс пользователя еще не готов, а если учесть всю мощь мок-объектов, то позволяет производить разрозненую разработку частей приложения.
Безбажных вам проектов.
Понравилось сообщение - подпишись на блог
15 комментариев:
Я бы не сказал, что все так хорошо. Например вы тестируете "голенький" spring контекст, а если разговор пойдет об тесте полноценного контекста для веб-приложения, то будут проблемы с tiles (бо, он гад пытается регистрироваться в sessioncontext-е, которой просто нет). будут проблемы с загрузкой messageboundles т.к. пути отсчитываются для веб относительно корня веб-приложения /WEB-INF/bla-bla, а при запуске теста от classpath всего этого нет.
И красивого способа, кроме создания пары наборов spring xml-ек, я не вижу, а вы?
Данная статья является скорее вводной и в ней затрагиваются в основном проблемы тестирования ДАО, бизнес-логики и прочих неинтерфейсных частей. Понятно, что когда мы начинаем работать с WEB все становится гораздо веселее. Но, думаю, со временем поделюсь опытом и в этой сфере.
Про пару наборов спринг-xml, я не вижу в этом проблемы. Добавляем в аннотацию еще один параметр и тем самым подключаем еще одну xml-ку.
Класс, Павел, спасибо за статью.
Сейчас как раз присматриваюсь к Spring'у. Пока больше к DAO, Acegi.
Есть желание перевести довольно-таки объемный проект на основу данного фрэймворка. Хотелось бы больше информации в этом направлении.
ЗЫ. Пользуясь моментом, пусть немного с опозданием, хочу поздравить тебя со cдачей диплома и получением статуса дипломированного специалиста. Удачи, творческих успехов тебе, продолжай в том же духе ;)
Сам пока только начинаю, но спринг уже успел понравится (правда раньше я его юзал, но все по мелочи. С нуля приложение на нем не проектировал). В скором времени надеюсь еще что-нибудь написать по его поводу )
Спасибо за поздравление!
А чем таким интересным будет отличаться описанный подход от наследования юнит теста от org.springframework.test.
AbstractTransactionalSpringContextTests ? Я юзал как раз второй подход.
Если честно, я с указаным вами подходом особо не разбирался, мне показалось, что там не поддерживаются аннотации и вся параметризация выполняется в коде, вызовом соответствующих методов. Все же ИМХО аннотированный код более удобочитаем. Плюс удобный механизм листенеров.
не обязательно имя 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
Господа, большое спасибо за ваши комментарии, они будут полезны как мне так и читателям блога.
З.Ы. Внес изменения в текст статьи по поводу setUp и tearDown
public class DemoSpringTestCase extends TestCase {
@Autowired
ISimpleBean mybean;
@Test
public void testInjectBlogDao() throws Exception {
System.out.println("BlogDAO = " + _blogDao);
assertNotNull(_blogDao);
}
}
Что за "_blogDao"? Autowired ведь "mybean"?
Большое спасибо за ваше замечание - поправил эту досадную опечатку.
на счет void prepareTestInstance(TestContext testContext) throws Exception;
здесь стоит еще добавить о наличии аннотаций @BeforeClass, @AfterClass
привет!
не подскажете, как в maven добавить dependency:
org.springframework.test
какой репозиторий использовать, чтобы пакет скачался этот?
Здравствуйте. К сожалению не подскажу, т.к. с maven почти не работал.
Отправить комментарий
Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!