пятница, 5 октября 2012 г.

Spring 3.1 + JPA + DBUnit, Gradle, Maven

В этой коротенькой статье-примере я хотел бы поделиться недавно приобретенным опытом, а именно:
  • Относительно новой возможности Spring Framework-a создавать так называемую Java based configuration, используя для этого фактически обычные POJO объекты с вкраплением анатаций от спринга
  • Написание Unit-тестов для тестировании базы данных. Рассмотрится несколько вариантов. Первый - тестирование базы с помощью Spring test context framework-a. Второй вариант - контекст, как в и первом случае, будет создавать SpringJUnit4ClassRunner.class из набора вышеупомянутого фреймворка, но для тестирования будет использоваться библиотека DBUnit.
  • Вначале проект специально был создан с использованием Maven-a, чтобы потом попробовать Gradle. Расскажу о полученных впечатлениях от Gradle и покажу конфигурацию того и другого.



В проекте были использованы последние на сегодняшний день (4 октября 2012) библиотеки:
  • junit 4.8.1
  • dbunit 2.4.8
  • springframework spring-test 3.1.2.RELEASE
  • springframework spring-orm 3.1.2.RELEASE
  • hsqldb 2.2.9
  • slf4j-log4j12 1.6.6
  • hibernate-jpa-2.0-api 1.0.0.Final
  • cglib 2.2.2
  • hibernate-entitymanager 4.1.6.Final
  • mysql-connector-java 5.1.21

Итак, структура проекта вот такая:

Начнем с первого в моем списке - с конфигурации. Главный конфигурационный класс находится в пакете ee.george.dbunit под именем ApplicationConfig.java:
package ee.george.dbunit;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ApplicationConfig {

}

Выглядит он довольно просто :-) поздже станет понятно какую смысловую нагрузку он собой несет.

Также в проекте присутствуют еще 2 конфигурационных файла.
Первый - ProductionDataConfig.java:

package ee.george.dbunit;

import java.util.Properties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.hibernate.ejb.HibernatePersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

@Configuration
@PropertySource("classpath:/application-prod.properties")
@Profile("prod")
@ComponentScan(basePackageClasses = ApplicationConfig.class)
public class ProductionDataConfig implements DataConfig {

 @Autowired
 private Environment env;

 @Override
 @Bean
 public DataSource dataSource() {
  SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
  dataSource.setDriverClass(com.mysql.jdbc.Driver.class);
  dataSource.setUrl(env.getProperty("db.url"));
  dataSource.setUsername(env.getProperty("db.username"));
  dataSource.setPassword(env.getProperty("db.password"));
  return dataSource;
 }

 @Override
 @Bean
 @Autowired
 public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
  LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();

  Properties jpaProperties = new Properties();
  jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQLDialect");
  jpaProperties.put("hibernate.show_sql", false);
  jpaProperties.put("hibernate.format_sql", false);

  bean.setPersistenceProviderClass(HibernatePersistence.class);
  bean.setJpaProperties(jpaProperties);
  bean.setDataSource(dataSource);
  bean.setPackagesToScan("ee.george.dbunit.model");
  return bean;
 }

 @Override
 @Bean
 @Autowired
 public JpaTransactionManager transactionManager(EntityManagerFactory emf, DataSource dataSource) {
  JpaTransactionManager bean = new JpaTransactionManager();
  bean.setDataSource(dataSource);
  bean.setEntityManagerFactory(emf);
  return bean;
 }

}

и второй - TestDataConfig.java:
package ee.george.dbunit;

import java.util.Properties;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.hibernate.ejb.HibernatePersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaDialect;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@PropertySource("classpath:/application-test.properties")
@Profile("test")
@EnableTransactionManagement
public class TestDataConfig implements DataConfig {

 @Autowired
 private Environment env;

 @Override
 @Bean
 public DataSource dataSource() {
  return new EmbeddedDatabaseBuilder().build();
 }

 @Override
 @Bean
 @Autowired
 public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
  LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();

  Properties jpaProperties = new Properties();
  jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.HSQLDialect");
  jpaProperties.put("hibernate.show_sql", false); // avoid double logging
  jpaProperties.put("hibernate.format_sql", true);
  jpaProperties.put("hibernate.hbm2ddl.auto", "create"); // auto initialization schema in database, based on JPA Entity classes

  bean.setPersistenceProviderClass(HibernatePersistence.class);
  bean.setDataSource(dataSource);
  bean.setJpaProperties(jpaProperties);
  bean.setPackagesToScan("ee.george.dbunit.model");

  return bean;
 }

 @Override
 @Bean
 @Autowired
 public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory, DataSource dataSource) {
  JpaTransactionManager bean = new JpaTransactionManager(entityManagerFactory);
  bean.setJpaDialect(new HibernateJpaDialect());
  bean.setDataSource(dataSource);
  return bean;
 }

}

Оба файла реализуют простенький интерфейс DataConfig:
package ee.george.dbunit;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;

import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;

public interface DataConfig {
 public DataSource dataSource();

 public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource);

 public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory, DataSource dataSource);

}

Как нетрудно догадаться по названиям, первый файл настраивает спринг на работу с настоящей базой на продакшине, второй говорит спрингу, чтобы тот в роли базы использовал базу данных в памяти и, если не указывать в бине dataSource() у создателя встроенной базы данных (EmbeddedDatabaseBuilder) setType(EmbeddedDatabaseType databaseType), - то будет использоваться база данных HSQLDB (HyperSQL DataBase). Также есть возможность использовать H2 и Apache Derby. В моем случае я не вижу причин использовать нестандартную базу, поэтому тип я не указываю.

Еще в конфигурации базы определены менеджер сущностей и менеджер транзакций. На самом деле по началу, когда сталкиваешься с этими именами типа бла-бла-Manager или бла-бла-ManagerFactory, то не особо понятно че это и зачем оно надо то. А объяснение на самом деле очень простое - менеджер сущностей умеет работать с вашими JPA Entity объектами. В данном случаем мы определям фабрику для производства этих менеджеров - она сама будет этих менеджеров клепать когда нужно. Ей только нужно указать (образно говоря) путь к базе (dataSource), диалект языка общения с базой данных, путь где лежат наши классы-сущности (Entities) и реализацию PersistenceProvider интерфейса. Persistence provider - это вообще сложнопонимаемая сразу штука из названия, так как на наш язык непойми как переводится. Но глядя на интерфейс можно увидеть, что он имеет всего пару методов для создания той самой фабрики о которой мы с вами говорим.
Я уже и так немного отошел от темы, но я надеюсь в целом ясно, касаемо EntityManager и его фабрики. С менеджером транзакций вообще просто. Что такое транзакция я надеюсь понятно, если нет - уделите пару часов Юре http://yuriytkach.blogspot.com/2009/11/java-persistence-api-jpa.html, он вам доступно всё расскажет. Я в проекте буду использовать входящий в комплект Spring ORM пакета JpaTransactionManager.

Надвания бинов определяется конвенциально по названиям методов. Не нужно писать никаких getDataSource() и getTransactionManager() - так как в этом слечае вы определите бины с соответствуюхими id. Название метода и есть название бина.

Еще одину вещь, которую я не упомянул - профили. Чрезвычайно увобная штука, которая избавляет нас от всяких извращений с проперти-файлами и написания своих костылей для работы с ними. Над классами ProductionDataConfig указываем @Profile("prod"), а над TestDataConfig "test" соответственно и перейдем непосредственно к тесту и посмотрим как сказать спрингу какую конфигурацию юзать:

package ee.george.dbunit.tests;

import static org.junit.Assert.assertEquals;

import javax.persistence.PersistenceException;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;


import ee.george.dbunit.ApplicationConfig;
import ee.george.dbunit.model.Person;
import ee.george.dbunit.service.PersonService;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@ActiveProfiles("test")
public class SpringTest extends AbstractTransactionalJUnit4SpringContextTests {

 @SuppressWarnings("unused")
 private static final Logger LOG = LoggerFactory.getLogger(SpringTest.class);

 @Autowired
 PersonService personService;

 @Test
 public void addPersonTest() {
  Person person = new Person();
  person.setFirstname("SOMENAME");

  personService.save(person);

  Person person2 = new Person();
  person2.setFirstname("SOMENAME2");

  personService.save(person2);

  Assert.assertEquals(2, countRowsInTable(Person.class.getSimpleName()));
 }

 // impossible to insert two persons with the same id
 @Test(expected = PersistenceException.class)
 public void addPersonThrowsExceptionTest() {
  Person person = new Person();
  person.setId(1);
  person.setFirstname("SOMENAME");

  personService.save(person);

  Person person2 = new Person();
  person2.setId(1);
  person2.setFirstname("SOMENAME");

  personService.save(person2);
 }

 @Test
 public void addPersonWithFather() {
  Person son = new Person();
  son.setFirstname("Andrew");
  son.setLastname("Smith");
  personService.save(son);

  Person father = new Person();
  father.setFirstname("Andrew father");
  personService.save(father);

  son.setFather(father);
  personService.save(son);

  assertEquals(2, countRowsInTable(Person.class.getSimpleName()));
 }
}


В принципе из примера должно быть понятно, что мы указываем наш класс конфигурации через анатацию @ContextConfiguration и указываем активный профиль test. Т.к. в класе ApplicationConfig в нас ничего нет (туда можно (и нужно) положить все средонезависимые бины), кроме анатации @ComponentScan, то в ней, собственно и фокус. Она сканирует все пакеты и подпакеты, которые мы ей укажем, а мы указали ee.george.dbunit - соответственно ProductionDataConfig и TestDataConfig тоже сюда попадают. Дальше Spring Context сам по профилю подключает нужные конфигурации, которые анатированы нужным профилем.

Наш тест наследует класс AbstractTransactionalJUnit4SpringContextTests, который всё, что делает - автоматически оборачивает каждые тест в транзакцию и откатывает ее при завершении теста. Еще он предоставляет пару удобных методов типа countRowsInTable(String tableName), но это мелочи. Если вы не ходите, чтобы транзакция роллбечилась, то над тестовым методом нужно указать анатацию @Rollback(false) или над всем тестовым классом @TransactionConfiguration(defaultRollback=false).
Вообщем-то тут должно быть всё понятно. Инжектим сервис PersonService, в котором заинжектин personDAO и работаем с ним.

package ee.george.dbunit.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import ee.george.dbunit.dao.AddressDAO;
import ee.george.dbunit.dao.PersonDAO;
import ee.george.dbunit.model.Address;
import ee.george.dbunit.model.Person;

@Service
@Transactional
public class PersonServiceImpl implements PersonService {

 @Autowired
 PersonDAO personDAO;

 @Autowired
 AddressDAO addressDAO;

 @Override
 public void addPersonAddress(int personId, Address address) {
  Person person = personDAO.findById(personId);
  if (person != null) {
   address.setPerson(personId);
   addressDAO.save(address);
  }
 }

 @Override
 public List getAllPersons() {
  return personDAO.findAll();
 }

 @Override
 public Person getPerson(int personId) {
  return personDAO.findById(personId);
 }

 @Override
 public void save(Person person) {
  personDAO.save(person);
 }

}

Теперь посмотрим на тест, который использует библиотеку DBUnit. Здесь я сделал несколько классов - абстрактный и, собственно, сам класс теста:

package ee.george.dbunit.tests;

import org.dbunit.JdbcBasedDBTestCase;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import ee.george.dbunit.ApplicationConfig;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { ApplicationConfig.class })
@ActiveProfiles("test")
abstract public class AbstractDBUnitTest extends JdbcBasedDBTestCase {

 static Logger LOG;

 public AbstractDBUnitTest() {
  LOG = LoggerFactory.getLogger(this.getClass());
 }

 @Override
 protected String getConnectionUrl() {
  return "jdbc:hsqldb:mem:testdb";
 }

 @Override
 protected IDataSet getDataSet() throws Exception {
  return new FlatXmlDataSetBuilder().build(getClass().getResourceAsStream("/TestDataSet.xml"));
 }

 @Override
 protected String getDriverClass() {
  return "org.hsqldb.jdbcDriver";
 }

 @Override
 protected String getPassword() {
  return "";
 }

 @Override
 public String getUsername() {
  return "sa";
 }
}

и второй:

package ee.george.dbunit.tests;

import java.sql.SQLException;

import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.After;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;

import ee.george.dbunit.model.Person;
import ee.george.dbunit.service.PersonService;

public class SomeDBUnitTest extends AbstractDBUnitTest {

 @Autowired
 PersonService personService;

 @After
 public void after() throws DatabaseUnitException, SQLException, Exception {
  DatabaseOperation.DELETE_ALL.execute(getConnection(), getDataSet());
 }

 @Test
 public void someTest() throws DataSetException, SQLException, Exception {
  Person person = new Person();
  person.setFirstname("Georgii");
  personService.save(person);

  IDataSet actualDataSet = getConnection().createDataSet();

//   Export dataset into the file
//  FlatXmlWriter writer = new FlatXmlWriter(new FileOutputStream("actualDataSet.xml"));
//  writer.setIncludeEmptyTable(false);
//  writer.write(actualDataSet);

//  FlatXmlDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(getClass().getResourceAsStream("/expectedDataSet.xml"));

  XmlDataSet expectedDataSet = new XmlDataSet(getClass().getResourceAsStream("/expectedDataSet.xml"));

  Assertion.assertEquals(expectedDataSet, actualDataSet);
 }
}

В абстрактном мы настраиваем конфигурацию, указываем профиль и наследуем абстрактный класс JdbcBasedDBTestCase из библиотеки DBUnit, который требует, чтобы мы реализовали несколько методов:

  • getConnectionUrl()
  • getDataSet()
  • getDriverClass()
  • getUsername()
  • getPassword()

В принципе тут всё ясно. Иерархия классов нашего теста выглядит вот так:

Суть теста по-моему не нуждается в пояснениях. Всё предельно ясно. Расскажу только про пару моментов, на которых я напоролся. Они касаются стркутуры XML-а, который вы будете сравнивать с тем, что сейчас в вашей базе.

Вначале я, сделал всё, как в примере с официального сайта:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <PERSON ID="1" FIRSTNAME="Georgii" />
</dataset>

и использовал для парсинга XML-a FlatXmlDataSet:
FlatXmlDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(getClass().getResourceAsStream("/expectedDataSet.xml"));

Тест мне выкинул ошибку, что количество таблиц разное:
junit.framework.ComparisonFailure: table count expected:<[1]> but was:<[2]>
 at org.dbunit.assertion.JUnitFailureFactory.createFailure(JUnitFailureFactory.java:39)
 at org.dbunit.assertion.DefaultFailureHandler.createFailure(DefaultFailureHandler.java:105)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:237)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:205)
 at org.dbunit.Assertion.assertEquals(Assertion.java:104)
 at ee.george.dbunit.tests.SomeDBUnitTest.someTest(SomeDBUnitTest.java:46)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

Окей, добавил таблицу:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <ADDRESS />
 <PERSON ID="1" FIRSTNAME="Georgii" />
</dataset>

Теперь такая ошибка - количество полей не совпадает:

junit.framework.ComparisonFailure: column count (table=PERSON, expectedColCount=2, actualColCount=5) expected:<[F[IRSTNAME, ID]]> but was:<[F[ATHER, FIRSTNAME, ID, LASTNAME, MOTHER]]>
 at org.dbunit.assertion.JUnitFailureFactory.createFailure(JUnitFailureFactory.java:39)
 at org.dbunit.assertion.DefaultFailureHandler.createFailure(DefaultFailureHandler.java:105)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:396)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:253)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:205)
 at org.dbunit.Assertion.assertEquals(Assertion.java:104)
 at ee.george.dbunit.tests.SomeDBUnitTest.someTest(SomeDBUnitTest.java:46)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

Хорошо. На тебе все поля:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <ADDRESS />
 <PERSON ID="1" FIRSTNAME="Georgii" FATHER="" MOTHER="" LASTNAME="" />
</dataset>

Тут я хочу заметить, что FlatXML.. (как его там?) парсилка считывает из первой записи все поля и в последующих можно их не указывать, если они пустые или равны null-ю.

Вообщем вроде теперь всё четко, но получаем такую ошибку:
junit.framework.ComparisonFailure: value (table=PERSON, row=0, col=LASTNAME) expected:<[]> but was:<[null]>
 at org.dbunit.assertion.JUnitFailureFactory.createFailure(JUnitFailureFactory.java:39)
 at org.dbunit.assertion.DefaultFailureHandler.createFailure(DefaultFailureHandler.java:105)
 at org.dbunit.assertion.DefaultFailureHandler.handle(DefaultFailureHandler.java:208)
 at org.dbunit.assertion.DbUnitAssert.compareData(DbUnitAssert.java:524)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:409)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:253)
 at org.dbunit.assertion.DbUnitAssert.assertEquals(DbUnitAssert.java:205)
 at org.dbunit.Assertion.assertEquals(Assertion.java:104)
 at ee.george.dbunit.tests.SomeDBUnitTest.someTest(SomeDBUnitTest.java:46)
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
 at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
 at java.lang.reflect.Method.invoke(Unknown Source)
 at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
 at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
 at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
 at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
 at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:74)
 at org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:31)
 at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:83)
 at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:72)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:231)
 at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
 at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
 at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
 at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
 at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
 at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
 at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
 at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:71)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
 at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:174)
 at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
 at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
 at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)


Говорит, что у нас в XML-e фамилия пустой стринг, а в базе null.
как это обойти? Может как-то и можно, но скажу чесно - у меня с полпинка не получилось, хотя чего-то в официальной доке и есть по этому поводу. Я пробовал говорить парсилке setColumnSensing(true) и null туда писать и [null] и даже NULL и [NULL]:). Ни-фи-га. В итоге я выкинул FlatXmlDataSet и заюзал обычный XmlDataSet, у которого есть понятнее некуда DTD (Document Type Definition).
В итоге expectedDataSet.xml у меня приобрел такой вид:
<?xml version='1.0' encoding='UTF-8'?>
<dataset>
 <table name="address" />
 <table name="person">
  <column>id</column>
  <column>firstname</column>
  <column>lastname</column>
  <column>father</column>
  <column>mother</column>
  <row>
   <value>1</value>
   <value>Georgii</value>
   <null />
   <null />
   <null />
  </row>
 </table>
</dataset>

и всё прекрасно заработало :-)
такие вот тонкости.

Ввиду своего малого (пока еще) опыта с базами данных - я пока мутно вижу реальное применение DBUnit'a, но чувствую, что оно таки есть :-) к тому-же у нас в компании его вовсю юзают, но я пока кода не видел, так как я на другом проекте. Надеюсь поздже пойму где ему место. Вообще складывается впечатление, что Spring вроде как двигается в это направлении - упрощение тестирования баз данных. В третьей версии вон сделали поддержку встроеной базы (Embedded database), напимер. Чего им стоит пару парселок XML-а добавить (ну, на самом деле DataSet не только из xml-a DBUnit может получить=) и сравнивалку налабать? И будет тот же самый DBUnit.

И еще один момент с которым мне пока не удалось разобраться - вначаеле у меня выполняются  Spring тесты, после которых транзакция откатывается. Потом выполняются DBUnit тесты. И вся проблема в автоинкременте. Несмотря на то, что транзакции в спринговых тестах роллбечятся - ID инкриментируется. т.е. в expectedDataSet.xml тест у меня валится, т.к. я до теста чищу базу, в тесте я добавляю одного человека и у него ID = 5 в базе. Как сбросить автоинкримент до теста - я пока не понял.

Теперь - что касается Gradle.

Сразу приведу исходный код билд скриптов для обоих:

MAVEN(pom.xml):
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">

 <modelVersion>4.0.0</modelVersion>
 <groupId>ee.george</groupId>
 <artifactId>dbunit-example</artifactId>
 <packaging>war</packaging>
 <version>1-SNAPSHOT</version>
 <name>DBUnit Example</name>

 <dependencies>
  <dependency>
   <groupId>org.slf4j</groupId>
   <artifactId>slf4j-log4j12</artifactId>
   <version>1.6.6</version>
  </dependency>

  <dependency>
   <groupId>junit</groupId>
   <artifactId>junit</artifactId>
   <version>4.8.1</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>org.dbunit</groupId>
   <artifactId>dbunit</artifactId>
   <version>2.4.8</version>
   <scope>test</scope>
  </dependency>

  <!-- JPA 2.0 -->
  <dependency>
   <groupId>org.hibernate.javax.persistence</groupId>
   <artifactId>hibernate-jpa-2.0-api</artifactId>
   <version>1.0.0.Final</version>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-test</artifactId>
   <version>3.1.2.RELEASE</version>
   <scope>test</scope>
  </dependency>

  <dependency>
   <groupId>org.springframework</groupId>
   <artifactId>spring-orm</artifactId>
   <version>3.1.2.RELEASE</version>
  </dependency>

  <!-- CGLIB is required to process @Configuration classes -->
  <dependency>
   <groupId>cglib</groupId>
   <artifactId>cglib</artifactId>
   <version>2.2.2</version>
  </dependency>

  <dependency>
   <groupId>org.hsqldb</groupId>
   <artifactId>hsqldb</artifactId>
   <version>2.2.9</version>
  </dependency>
  <dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-entitymanager</artifactId>
   <version>4.1.6.Final</version>
  </dependency>

  <dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.21</version>
  </dependency>

 </dependencies>

 <build>
  <plugins>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>2.5.1</version>
    <configuration>
     <source>1.6</source>
     <target>1.6</target>
     <encoding>UTF-8</encoding>
     <showWarnings>true</showWarnings>
     <showDeprecation>true</showDeprecation>
    </configuration>
   </plugin>
   <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-eclipse-plugin</artifactId>
    <version>2.9</version>
    <configuration>
     <downloadSources>true</downloadSources>
    </configuration>
   </plugin>
  </plugins>
 </build>

 <repositories>
  <repository>
   <id>hsqldb</id>
   <url>http://www.hsqldb.org/repos/</url>
  </repository>
 </repositories>
</project>

GRADLE(build.gradle):
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'war'

sourceCompatibility = 1.7
targetCompatibility = 1.7
version = '1.0'

eclipse {
 classpath {
  downloadSources = true
     downloadJavadoc = true
 }
}

repositories {
    mavenCentral()
    maven {
     url "http://www.hsqldb.org/repos/"
    }
}

dependencies {
    testCompile 'junit:junit:4.8.1'
    testCompile 'org.dbunit:dbunit:2.4.8'
    testCompile 'org.springframework:spring-test:3.1.2.RELEASE'
    testCompile 'org.hsqldb:hsqldb:2.2.9'
    compile 'org.slf4j:slf4j-log4j12:1.6.6'
    compile 'org.hibernate.javax.persistence:hibernate-jpa-2.0-api:1.0.0.Final'
    compile 'org.springframework:spring-orm:3.1.2.RELEASE'
    compile 'cglib:cglib:2.2.2'
    compile 'org.hibernate:hibernate-entitymanager:4.1.6.Final'
    compile 'mysql:mysql-connector-java:5.1.21'
}

Maven vs Gradle? - вот в чем вопрос :-)

Я могу дать оценку только с точки зрения человека, который знает мавин, как свои пять пальцев, в gradle, как Отче Наш.

Gradle оказался существенно короче в синтаксисе, и у Gradle великолепная дока на официальном сайта - душа и мозг искренне порадовалась. Конвертация pom.xml в build.gradle у меня заняла около часа-полутора и оказалась довольно простой. Вообщем-то осталось такое.. нейтральное впечатление, типа "ну короче, да, и? в чем поинт?". Ответа на вопрос нахера мне gradle я пока для себя не вывел. В Eclipse поддержка практически нулевая. Я поставил какой-то такой
Gradle IDE 3.0.0.201208090949-RELEASE org.springsource.ide.eclipse.gradle.feature.feature.group SpringSource, a division of VMware, Inc.
плагин, создал вручную файл build.gradle ну и вот собственно какие плюшки оно мне дало:
То есть никаких. Мало того что пункты меню какие-то бестолковые, так еще и неактивные почему-то.
Но профи массово тащацца от gradle. Ну, им виднее.


Архив с проектом можно взять тут - DBUnitExample.zip (File > Download).
Для установки раззипуйте архив и положите его в Workspace эклипса. Дальше для Gradle введите команду 'gradle e', для мавина 'mvn eclipse:eclipse'. После этого стандартно из Eclise - File -> Import -> Existing projects into Workspace.

2 комментария:

  1. @Override
    protected IDataSet getDataSet() throws Exception {
    return new FlatXmlDataSetBuilder().build(getClass().getResourceAsStream("/TestDataSet.xml"));
    }

    Пока не заменил на:

    @Override
    protected IDataSet getDataSet() throws Exception {
    return new XmlDataSet(new FileInputStream("/TestDataSet.xml"));
    }

    Выдавало ошибку:
    NoSuchTableException: value
    Причем value это элемент xml.

    ОтветитьУдалить