Статьи

Тестирование баз данных с помощью JUnit и Hibernate. Часть 1. Править ими

Базы данных являются чрезвычайно важной частью почти каждого корпоративного приложения. Тем не менее, существует очень небольшая поддержка для тестирования вашей базы данных, что приводит к очень небольшому охвату тестов кода, связанного с базой данных. В отчаянной попытке изменить это хотя бы немного в серии статей, начиная с этой статьи, будут описаны некоторые проблемы и возможные частичные решения на основе 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 и ее зависимостях.

 

Из http://blog.schauderhaft.de/2011/03/13/testing-databases-with-junit-and-hibernate-part-1-one-to-rule-them/