Предыдущая запись | Следующая запись

Про полезность подхода TDD (разработка через тестирование, test driven development) не слышал только ленивый или глухой. Но сегодня мы не будем обсуждать всю его полезность и красоту, а также проблемы и недостатки. Сегодня мы попробуем посмотреть, как разрабатывать unit-тесты для spring приложений. Также мы немного тронем ручное управление транзакциями в unit-тестах.
Небольшое замечание: иногда тесты spring приложений это не совсем unit-тесты, потому что мы можем в них поднять и задействовать очень сложное окружение (БД, WebService и так далее). Подобные тесты это скорее интеграционные тесты, но я думаю что сейчас философские вопросы мы поднимать не будем.
Для начала предлагаю согласовать некоторые термины и понятия.
- Unit-тест – тест, который проверяет поведение небольшой части приложения. Эта часть может быть одним классом, одним методом или набором классов, который реализуют какое-то архитектурное решение, и это решение необходимо проверить на работоспособность. За подробностями обращайтесь сюда или туда.
- Application context config – конфигурационный файл в xml формате для описания структуры spring приложения. Про spring читаем тут или там.
- DAO – объект доступа к данным или data acess object, хотя некоторым нравится употреблять данный термин в значении “путь”. Основное предназначение этого шаблона проектирования: связать вместе БД и наше приложение. За подробностями идем сюда или туда.
- Транзакция – группа последовательных операций, которая представляет собой логическую единицу работы с данными. Транзакция может быть выполнена либо целиком и успешно, соблюдая целостность данных и независимо от параллельно идущих других транзакций, либо не выполнена вообще и тогда она не должна произвести никакого эффекта. Про транзакции читаем тут или там.
Все остальные термины и понятия стандартны и давно устоялись или их описание тут некритично, а если нет, то у нас есть простор для hollywar. Например, такое понятие как IoC (инверсия управления, inversion of control).
Совсем забыл, про написание unit-тестов для spring-приложений я пишу не первый, немного есть тут и там.
Итак, у нас стоит цель: протестировать поведение класса в spring-приложении, дополнительно необходимо вручную управлять транзакциями. Для этого мы создадим простое spring-приложение и напишем unit-тест. Наш unit-тест при запуске будет инициализировать application context config нашего spring приложения и после этого вызывать методы у тестируемого нами класса. Также мы разработаем отдельный тест, в котором будем управлять транзакциями вручную.
Технологии в приложении будут следующие:
- Средство сборки и компиляции – Apache Maven.
- База данных – HSQLDB.
- Средство для отображения классов в базу данных (Object relation mapping) – Hibernate.
- Средство для конфигурирования приложения – Spring framework.
- Средство для создания unit-тестов – JUnit.
Конечная структура файлов в приложении будет выглядеть следующим образом:
Кстати вы можете загрузить себе (из SVN) и посмотреть в браузере исходные коды работающего приложения.
Начнем?
Во первых, нам надо создать pom.xml в котором мы опишем сборку и компиляцию приложения (про maven читать тут или там). В данном конфигурационном файле мы пропишем все зависимости от используемых нами библиотек. Также на данном шаге мы создадим все директории нашего приложения.
Во вторых, мы создадим java persistente entity класс – ru.intr13.example.springTransactionalTes
В третьих, мы создадим интерфейс – ru.intr13.example.springTransactionalTes
В четвертых, мы создадим реализацию разработанного нами интерфейса – ru.intr13.example.springTransactionalTes
В пятых, мы создадим 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.springTransactionalTes
Но для полноценного тестирования нам требуется чтобы у нашего теста была возможность получить разработанный нами сервис. Для этого мы прописываем в нашем тесте поле со ссылкой на сервис и ставим для этого поля аннотацию – @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(Transaction
и потом делаем для нее commit (сохранение):
transactionManager.commit(transaction);
или rollback (откат):
transactionManager.rollback(transaction)
Зная все выше приведенное, мы можем написать следующий тест, который создает несколько транзакций вручную:
@Test
public void comlexTest() {
TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition(Transaction
String text = UUID.randomUUID().toString();
dataDao.save(new Data(text));
transactionManager.commit(transaction);
transaction = transactionManager.getTransaction(new DefaultTransactionDefinition(Transaction
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
Картинка найдена здесь. Кстати внимательный человек заметит одну забавную вещь:)
Originally published at Исследователь бытия. You can comment here or there.


Comments
Оговорю себя на всякий случай, но вдруг я прав...
Со Spring+Hibernate+JUnit работаю только 2 недели, до этого же на Java пе программировал 5 лет. И да, я совершенно не в курсе как работать с аннотациями. ну и т.д. в том же духе...
Но хватит про грустное...
Сам я только-только решил для себя проблему тестирования DAO в Spring приложении вот как.
Я отнаследовал базовый клас тестов для DAO слоя от AbstractTransactionalDataSourceSpringCon
Вот что про него в API reference написано
1. Ability to delete or insert any data in the database, without affecting other tests
2. Providing a transactional context for any code requiring a transaction
3. Ability to write anything to the database without any need to clean up.
Что удобно в нём, так это то, что транзакции стартуют в начале класса и все изменеия сделанные в БД откатываются в конце работы теста.
Транзакциеями внутри теста можно управлять при помощи методов startNewTransaction() и endTransaction()
По мне, код получается значительно чище:
Дальше рабочий код:
Для примера, в методе теста PersonDAoTest testAddAndRemovePerson() я как раз прерываю транзакцию чтобы проверить работу выброшенного мною исключения.
Вот это код DAO класса...
/*Java native imports*/
import java.util.List;
import java.util.Vector;
/*Spring imports*/
import org.springframework.orm.hibernate3.suppo
import org.springframework.orm.ObjectRetrievalF
/*log4j imports*/
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/*DAO import*/
import edu.khai.teachingstaff.dao.PersonDAO;
/*Model import*/
import edu.khai.teachingstaff.model.Person;
public class PersonDAOHibernate extends HibernateDaoSupport implements PersonDAO
{
private static Log log = LogFactory.getLog(PersonDAOHibernate.cla
public List getPersons(){
return getHibernateTemplate().find("from Person");
}
public List getScientificDegrees(Long idPerson){
return
getHibernateTemplate().find("from ScientificDegree as sd where sd.person.idPerson=?", idPerson);
}
public Person getPerson(Long idPerson){
Person person = (Person)getHibernateTemplate().get(Perso
if (person == null) {
throw new ObjectRetrievalFailureException(Person.c
}
return person;
}
public void savePerson(Person person)
{
getHibernateTemplate().saveOrUpdate(pers
if (log.isDebugEnabled()){
log.debug("idPerson set to :: " + person.getIdPerson());
}
}
public void removePerson(Long idPerson){
Person person = (Person)getHibernateTemplate().load(Pers
//getHibernateTemplate().initialize(pers
getHibernateTemplate().delete(person);
}
}
Базовый класс BaseDAOTestCase:
package edu.khai.teachingstaff.dao;
/*Junit imports*/
import junit.framework.TestCase;
/*log4j imports*/
import org.apache.log4j.Logger;
/*Spring imports*/
import org.springframework.context.ApplicationC
import org.springframework.context.support.Clas
/*Base Test class for DAO testing from Spring*/
import org.springframework.test.AbstractTransac
/**
* Base class for DAO TestCases.
* @author Quester
*/
public class BaseDAOTestCase extends AbstractTransactionalDataSourceSpringCon
protected final Logger log = Logger.getLogger(getClass());
protected String[] getConfigLocations() {
setAutowireMode(AUTOWIRE_BY_NAME);
return new String[] {"teachingstaff-servlet.xml"};
}
}
И собственно Testcase для PersonDAO:
package edu.khai.teachingstaff.dao;
/*java native imports*/
import java.util.List;
import java.util.GregorianCalendar;
/*DAO import*/
import edu.khai.teachingstaff.dao.PersonDAO;
/*Model import*/
import edu.khai.teachingstaff.model.Person;
/*Spring DAO access exception imports*/
import org.springframework.dao.DataAccessExcept
public class PersonDAOTest extends BaseDAOTestCase{
private Person person = null;
private PersonDAO personDAO = null;
public void setPersonDAO(PersonDAO personDAO) {
this.personDAO = personDAO;
}
public void testGetPersons()throws Exception{
//Делается person
person = new Person("TestLastNameUkr",
"TestNameUkr",
"TestSecondNameUkr",
new GregorianCalendar(2000, 1, 1));
//сохраняем человека
personDAO.savePerson(person);
//Проверяем, условие того, что список людей >=1;
//а он точно должен быть больше чем 1, если в базе для Person что-то есть
assertTrue(personDAO.getPersons().size()
}
public void testSaveUser() throws Exception {
//Делается person
person = new Person("TestLastNameUkr",
"TestNameUkr",
"TestSecondNameUkr",
new GregorianCalendar(2000, 1, 1));
//сохраняю person
personDAO.savePerson(person);
//проверяю person на наличие первичного ключа назначенного Hibernate
assertTrue("primary key assigned", person.getIdPerson() != null);
//проверяю одно из полей на наличие данных
assertNotNull(person.getLastNameUkr());
}
public void testAddAndRemovePerson() throws Exception{
//Запомнить!!! По умолчанию в начале теста всегда стартует транзакция
//В конце теста транзакция закроется, если её не прервать endTransaction();
//например для проверки работы ленивой загрузки коллекций
//Добавляем человека
person = new Person("TestLastNameUkr",
"TestNameUkr",
"TestSecondNameUkr",
new GregorianCalendar(2000, 1, 1));
//сохраняем человека
personDAO.savePerson(person);
//проверяем, установлено ли id для человека
//и проверяем одно из полей на ищдентичность заданным параметрам
assertNotNull(person.getIdPerson());
assertTrue(person.getLastNameUkr().equal
//выводим инфу о person в лог
log.info(person);
log.debug("removing person...");
//Person удаляется
personDAO.removePerson(person.getIdPerso
//Вот здесь надо закончить транзакцию для того чтобы проверить,
//есть ли в базе созданная на этапе тестирования запись
log.debug("endTransaction() call...");
endTransaction();
//Если в базе нет записи и производится попытка её получить, то выбрасывается ожидаемое исключение,
//которое обрабатывается и выводится
//Если запись в базе окажется, то тест будет провален через Fail
try {
person = personDAO.getPerson(person.getIdPerson()
log.debug("Try...");
fail("User found in database");
} catch (DataAccessException dae) {
log.debug("catch...");
log.debug("Expected exception: " + dae.getMessage());
assertNotNull(dae);
}
}
}