Это последняя часть в этой небольшой серии о тестировании кода базы данных.
В первой части я извлек обработку сессий для тестов в правило JUnit.
Во второй части я представил ObjectMothers для легкого создания экземпляров в базе данных.
В этой части я упросту реализацию новых ObjectMothers, извлекая два суперкласса, и я закончу наброском некоторых дальнейших идей для дальнейшего развития.
Давайте посмотрим на SuperHeroMother
public class SuperHeroMother {
private final Session session;
private String secretIdentity = "Mr. Jones";
private String name = "Name";
private String weakness = "None";
private SuperPower power = null;
public SuperHeroMother(Session s) {
session = s;
power = new SuperPowerMother(session).instance();
}
public SuperHero instance() {
SuperHero hero = loadInstance();
if (hero == null) {
hero = createInstance();
}
hero.power = power;
hero.weakness = weakness;
hero.secretIdentity = secretIdentity;
session.save(hero);
return hero;
}
private SuperHero loadInstance() {
return (SuperHero) session.createCriteria(SuperHero.class)
.add(Restrictions.eq("name", name)).uniqueResult();
}
private SuperHero createInstance() {
SuperHero hero = new SuperHero();
hero.name = name;
return hero;
}
public SuperHeroMother name(String aName) {
name = aName;
return this;
}
public SuperHeroMother secretIdentity(String aSecretIdentity) {
secretIdentity = aSecretIdentity;
return this;
}
public SuperHeroMother power(SuperPower aPower) {
power = aPower;
return this;
}
public SuperHeroMother weaknes(String aWeakness) {
weakness = aWeakness;
return this;
}
}
Есть еще некоторый код, который будет повторяться снова и снова для каждой ObjectMother. Итак, давайте извлечем некоторые общие черты в суперкласс ObjectMother:
/** create instances of type <tt>T</tt> */
public abstract class ObjectMother<T> {
private final Session session;
public ObjectMother(Session s) {
session = s;
}
/** returns an instance based on the configuration of this object mother */
public T instance() {
T t = loadInstance(session);
if (t == null)
t = createInstance();
configureInstance(t);
session.save(t);
return t;
}
/**
* configure the instance <tt>t</tt> according to the configuration of this
* ObjectMother
*/
abstract protected void configureInstance(T t);
/**
* try to load an instance based on the alternate key. Returns null if no
* such instance exists
*/
abstract protected T loadInstance(Session session);
/**
* create a fresh instance with the alternate key set according to the
* configuration of this ObjectMother
*/
abstract protected T createInstance();
}
ObjectMother использует шаблон метода шаблона для координации предоставления экземпляров: попробуйте загрузить соответствующий экземпляр из базы данных, если это не сработает, создайте его с нуля, настройте все атрибуты экземпляра, сохраните его в базе данных и верни это. Загрузка, создание и настройка должны быть реализованы в подклассах.
Для трех сущностей, используемых в этой серии, мы можем извлечь еще больше кода из различных ObjectMothers, потому что загрузка экземпляра из базы данных всегда работает одинаково. Но это только так, потому что у нас есть альтернативный ключ, основанный на одном атрибуте. Это будет часто, но не всегда, поэтому я не хочу связывать это с ObjectMother. Вместо этого я создам еще один суперкласс для этого особого вида ObjectMother: SingleAlternateKeyObjectMother
public abstract class SingleAlternateKeyObjectMother<T, A, S extends SingleAlternateKeyObjectMother<T, A, S>>
extends ObjectMother<T> {
private final Class<T> objectType;
private final String alternateKeyName;
private A alternateKey;
public SingleAlternateKeyObjectMother(Session s, Class<T> theObjectType,
A defaultAlternateKey, String theAlternateKeyName) {
super(s);
objectType = theObjectType;
alternateKeyName = theAlternateKeyName;
alternateKey(defaultAlternateKey);
}
@SuppressWarnings("unchecked")
@Override
final protected T loadInstance(Session session) {
return (T) session.createCriteria(objectType)
.add(Restrictions.eq(alternateKeyName, getAlternateKey()))
.uniqueResult();
}
@SuppressWarnings("unchecked")
public S alternateKey(A theAlternateKey) {
alternateKey = theAlternateKey;
return (S) this;
}
public A getAlternateKey() {
return alternateKey;
}
}
При этом SuperHeroMother содержит только пару чрезвычайно простых методов, практически не оставляя дублирования кода:
public class SuperHeroMother extends
SingleAlternateKeyObjectMother<SuperHero, String, SuperHeroMother> {
private String secretIdentity = "Mr. Jones";
private String weakness = "None";
private SuperPower power;
public SuperHeroMother(Session s) {
super(s, SuperHero.class, "Name", "name");
power = new SuperPowerMother(s).instance();
}
@Override
protected void configureInstance(SuperHero hero) {
hero.power = power;
hero.weakness = weakness;
hero.secretIdentity = secretIdentity;
}
@Override
protected SuperHero createInstance() {
SuperHero hero = new SuperHero();
hero.name = getAlternateKey();
return hero;
}
public SuperHeroMother secretIdentity(String aSecretIdentity) {
secretIdentity = aSecretIdentity;
return this;
}
public SuperHeroMother power(SuperPower aPower) {
power = aPower;
return this;
}
public SuperHeroMother weaknes(String aWeakness) {
weakness = aWeakness;
return this;
}
}
Обратите внимание, что три последних метода необходимы только тогда, когда вы хотите контролировать эти три атрибута в своих тестах. Кроме того, эти методы действительно страдают от дублирования кода, но я не вижу способа абстрагироваться от этого дублирования, не используя много размышлений, которые я не считаю целесообразными в этом случае.
При такой настройке инфраструктуры не должно быть оснований оставлять какие-либо операторы SQL непроверенными. Хотя есть несколько особых случаев.
Как уже упоминалось в первой части этой серии, вы должны использовать базу данных в памяти для этих тестов. Это на порядок быстрее, чем обычная постоянная база данных. С базой данных в памяти (HSQLDB) мы выполняем около 400 тестов за 5 минут в текущем проекте. Я считаю это медленным как грязь для стандартов модульных тестов, но сравните это с 90 минутами для тех же тестов с базой данных Oracle. Но иногда у вас могут быть операторы SQL, специфичные для базы данных. Например, мы используем аналитические функции, которые являются чрезвычайно мощными, но не поддерживаются HSQLDB и, насколько я знаю, никакими другими свободными в памяти базами данных. Чтобы протестировать эти операторы и по-прежнему иметь быстро работающий набор тестов, мы дополнительно расширили наше правило фабричной сессии.Он проверяет, помечен ли тестовый метод или тестовый класс специальной аннотацией (@OracleTest), а также проверяет SqlDialect объекта HibernateConfiguration. Если аннотация присутствует, но SqlDialect не является диалектом Oracle, тест не выполняется. Наша система непрерывной интеграции (Jenkins) имеет два задания для теста: одно настроено с базой данных Oracle и одно с HSQLDB в базе данных памяти. Последний дает быструю обратную связь для каждой проверки в системе контроля версий, а вторая запускает медленные, но более полные тесты.Последний дает быструю обратную связь для каждой проверки в системе контроля версий, а вторая запускает медленные, но более полные тесты.Последний дает быструю обратную связь для каждой проверки в системе контроля версий, а вторая запускает медленные, но более полные тесты.
Хотя материал, представленный здесь, на мой взгляд, чрезвычайно полезен для модульных тестов доступа к базе данных, есть другие вещи, которые требуют тестирования в контексте баз данных. Вы должны особенно рассмотреть тесты для ваших операторов ddl и тесты производительности. Для тестов утверждений ddl я привел идеи в предыдущей статье. Для тестов производительности подход ObjectMothers по крайней мере в его нынешнем виде в основном бесполезен, потому что создание больших объемов данных было бы способом замедлить. Поэтому здесь нужны специальные техники. Может быть, кто-нибудь может предоставить полезные ссылки в комментариях?
Из http://blog.schauderhaft.de/2011/03/27/testing-databases-with-junit-and-hibernate-part-3-cleaning-up-and-future-ideas/