Статьи

Тестирование баз данных с помощью JUnit и Hibernate. Часть 3. Очистка и дальнейшие идеи

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