Это последняя часть в этой небольшой серии о тестировании кода базы данных.
В первой части я извлек обработку сессий для тестов в правило 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/