Базы данных являются чрезвычайно важной частью почти каждого корпоративного приложения. Тем не менее, существует очень небольшая поддержка для тестирования вашей базы данных, что приводит к очень небольшому охвату тестов кода, связанного с базой данных. В отчаянной попытке изменить это хотя бы немного в серии статей, начиная с этой статьи, будут описаны некоторые проблемы и возможные частичные решения на основе Hibernate и JUnit.
В качестве примера я использую следующий небольшой набор классов сущностей Hibernate, и из него будет создана схема базы данных Hibernate:
@Entity
public class SuperHero extends AbstractEntity {
@NotEmpty
@Column(unique = true)
public String name;
@ManyToOne
@NotNull
public SuperPower power;
@NotEmpty
public String weakness;
@NotEmpty
public String secretIdentity;
}
@Entity
public class SuperPower extends AbstractEntity {
@NotEmpty
@Column(unique = true)
public String name;
@NotEmpty
public String description;
@NotNull
@ManyToOne
public SuperPowerType type;
}
@Entity
public class SuperPowerType extends AbstractEntity {
@NotEmpty
@Column(unique = true)
public String name;
@NotNull
@Length(min = 30)
public String description;
}
Как вы можете видеть (и если вы этого не скажете), SuperHero ссылается на SuperPower, а SuperPower ссылается на SuperPowerType. У всех есть некоторые обязательные атрибуты.
Все три объекта имеют общий супертип, предоставляющий идентификатор и версию.
@MappedSuperclass
public class AbstractEntity {
@SuppressWarnings("unused")
@Id
@GeneratedValue
public Long id;
@SuppressWarnings("unused")
@Version
private Long version;
}
Для краткости я пропустил геттеры и сеттеры.
Наконец, есть SuperHeroRepository, который я хочу протестировать. У него есть единственный метод, возвращающий SuperHero на основе SuperPower
. Простой тест для SuperHeroRepository может работать так:
Создайте SuperHero и убедитесь, что я могу получить его снова, используя SuperHeroRepository.
Легко, не правда ли? Что ж… давайте посмотрим, что вы действительно должны сделать
- создать пустую базу данных
- создайте SuperPowerType, убедившись, что все обязательные поля заполнены
- создать SuperPower этого типа, убедившись, что у вас есть все обязательные поля заполнены
- создайте SuperHero, убедившись, что все обязательные поля заполнены
- использовать хранилище для получения SuperHeros
- утверждаю, что вы получили ожидаемый результат.
Таким образом, мы должны заполнить все обязательные поля и предоставить SuperPowerType (опять же со всеми обязательными полями), хотя ничего в тесте не касается SuperPowerTypes. Если вы все еще не верите, что это приводит к ужасному коду, взгляните на эту наивную реализацию.
public class SuperHeroRepository1Test {
private SessionFactory sessionFactory;
private Session session = null;
@Before
public void before() {
// setup the session factory
AnnotationConfiguration configuration = new AnnotationConfiguration();
configuration.addAnnotatedClass(SuperHero.class)
.addAnnotatedClass(SuperPower.class)
.addAnnotatedClass(SuperPowerType.class);
configuration.setProperty("hibernate.dialect",
"org.hibernate.dialect.H2Dialect");
configuration.setProperty("hibernate.connection.driver_class",
"org.h2.Driver");
configuration.setProperty("hibernate.connection.url", "jdbc:h2:mem");
configuration.setProperty("hibernate.hbm2ddl.auto", "create");
sessionFactory = configuration.buildSessionFactory();
session = sessionFactory.openSession();
}
@Test
public void returnsHerosWithMatchingType() {
// create the objects needed for testing
SuperPowerType powerType = new SuperPowerType();
powerType.name = "TheType";
powerType.description = "12345678901234567890aDescription";
SuperPower superpower = new SuperPower();
superpower.name = "SuperPower";
superpower.description = "Description";
superpower.type = powerType;
SuperHero hero = new SuperHero();
hero.name = "Name";
hero.power = superpower;
hero.weakness = "None";
hero.secretIdentity = "Mr. Jones";
// storing the objects for the test in the database
session.save(powerType);
session.save(superpower);
session.save(hero);
SuperHeroRepository heroRepository = new SuperHeroRepository(session);
List<SuperHero> heroes = heroRepository.loadBy(superpower);
assertNotNull(heroes);
assertEquals(1, heroes.size());
}
@After
public void after() {
session.close();
sessionFactory.close();
}
}
В этом тесте я могу сказать только одно: он использует H2 в режиме In-Memory, поэтому он достаточно быстрый. Есть первый урок: * Используйте базу данных в памяти для тестирования, если это возможно. Это легко на 100 * быстрее, чем база данных на диске *
Итак, очевидно, что мы хотим реорганизовать этого монстра, который после некоторого метода может извлечь результат примерно так:
public class SuperHeroRepository2Test {
private SessionFactory sessionFactory;
private Session session;
@Before
public void before() {
sessionFactory = createSessionFactory();
session = sessionFactory.openSession();
Transaction transaction = session.beginTransaction();
}
@Test
public void returnsHerosWithMatchingType() {
SuperPowerType powerType = createSuperPowerType();
session.save(powerType);
SuperPower superpower = createSuperPower(powerType);
session.save(superpower);
SuperHero hero = createSuperHero(superpower);
session.save(hero);
SuperHeroRepository heroRepository = new SuperHeroRepository(session);
List<SuperHero> heroes = heroRepository.loadBy(superpower);
assertNotNull(heroes);
assertEquals(1, heroes.size());
}
private SessionFactory createSessionFactory() {
AnnotationConfiguration configuration = new AnnotationConfiguration();
configuration.addAnnotatedClass(SuperHero.class)
.addAnnotatedClass(SuperPower.class)
.addAnnotatedClass(SuperPowerType.class);
configuration.setProperty("hibernate.dialect",
"org.hibernate.dialect.H2Dialect");
configuration.setProperty("hibernate.connection.driver_class",
"org.h2.Driver");
configuration.setProperty("hibernate.connection.url", "jdbc:h2:mem");
configuration.setProperty("hibernate.hbm2ddl.auto", "create");
SessionFactory sessionFactory = configuration.buildSessionFactory();
return sessionFactory;
}
private SuperHero createSuperHero(SuperPower superpower) {
SuperHero hero = new SuperHero();
hero.name = "Name";
hero.power = superpower;
hero.weakness = "None";
hero.secretIdentity = "Mr. Jones";
return hero;
}
private SuperPower createSuperPower(SuperPowerType powerType) {
SuperPower superpower = new SuperPower();
superpower.name = "SuperPower";
superpower.description = "Description";
superpower.type = powerType;
return superpower;
}
private SuperPowerType createSuperPowerType() {
SuperPowerType powerType = new SuperPowerType();
powerType.name = "TheType";
powerType.description = "12345678901234567890aDescription";
return powerType;
}
}
Это было бы хорошо, если бы это было единственное испытание такого рода. Но для другого подобного теста также необходим SessionFactory, поэтому я перенесу все элементы SessionFactory, Session и Transaction в правило
public class SessionFactoryRule implements MethodRule {
private SessionFactory sessionFactory;
private Transaction transaction;
private Session session;
@Override
public Statement apply(final Statement statement, FrameworkMethod method,
Object test) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
sessionFactory = createSessionFactory();
createSession();
beginTransaction();
try {
statement.evaluate();
} finally {
shutdown();
}
}
};
}
private void shutdown() {
try {
try {
try {
transaction.rollback();
} catch (Exception ex) {
ex.printStackTrace();
}
session.close();
} catch (Exception ex) {
ex.printStackTrace();
}
sessionFactory.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
private SessionFactory createSessionFactory() {
AnnotationConfiguration configuration = new AnnotationConfiguration();
configuration.addAnnotatedClass(SuperHero.class)
.addAnnotatedClass(SuperPower.class)
.addAnnotatedClass(SuperPowerType.class);
configuration.setProperty("hibernate.dialect",
"org.hibernate.dialect.H2Dialect");
configuration.setProperty("hibernate.connection.driver_class",
"org.h2.Driver");
configuration.setProperty("hibernate.connection.url", "jdbc:h2:mem");
configuration.setProperty("hibernate.hbm2ddl.auto", "create");
SessionFactory sessionFactory = configuration.buildSessionFactory();
return sessionFactory;
}
public Session createSession() {
session = sessionFactory.openSession();
return session;
}
public void commit() {
transaction.commit();
}
public void beginTransaction() {
transaction = session.beginTransaction();
}
public Session getSession() {
return session;
}
}
Что оставляет тест в несколько приемлемом состоянии.
public class SuperHeroRepository3Test {
@Rule
public final SessionFactoryRule sf = new SessionFactoryRule();
@Test
public void returnsHerosWithMatchingType() {
Session session = sf.getSession();
SuperPowerType powerType = createSuperPowerType();
session.save(powerType);
SuperPower superpower = createSuperPower(powerType);
session.save(superpower);
SuperHero hero = createSuperHero(superpower);
session.save(hero);
sf.commit();
SuperHeroRepository heroRepository = new SuperHeroRepository(session);
List<SuperHero> heroes = heroRepository.loadBy(superpower);
assertNotNull(heroes);
assertEquals(1, heroes.size());
}
// … private methods more or less as before
}
Все создание SessionFactory теперь перенесено в правило и поэтому идеально подходит для повторного использования. Теперь вы можете изменить способ создания вашей SessionFactory в одной точке для всех тестов, нуждающихся в базе данных. Это хорошо, и этого будет достаточно на сегодня. На следующей неделе мы позаботимся о нашем SuperHero и ее зависимостях.