null

Тестирование EJB с помощью JUnit и OpenEJB Embedded

Часто возникает необходимость протестировать всю цепочку взаимодействия, от вызова метода в EJB бине до записи данных в базу. При тестировании классов EJB не обойтись без EJB-контейнера, управляющего их жизненным циклом. Для этих целей можно использовать OpenEJB - встраиваемый EJB-контейнер от Apache, входящий в сервер приложений TomEE.
Рассмотрим простой пример, в котором есть класс сущности, бин для работы с этой сущностью и тестовый класс для тестирования методов бина. В примере используется Hibernate и управление транзакциями возложено на контейнер (CMP).

Файл persistence.xml.

<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             version="2.0"
             xmlns="http://java.sun.com/xml/ns/persistence"
             xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
  <persistence-unit name="com.tuneit.event.PU" transaction-type="JTA">
    <provider>org.hibernate.ejb.HibernatePersistence</provider>
    <jta-data-source>jdbc/DBPool</jta-data-source>
    <exclude-unlisted-classes>false</exclude-unlisted-classes>
    <properties>
      <property name="hibernate.hbm2ddl.auto" value="update"/>
    </properties>
  </persistence-unit>
</persistence>

 

Добавляем необходимые зависимости в pom.xml.

<dependencies>
  <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>5.0.2.Final</version>
  </dependency>
  <dependency>
      <groupId>org.apache.openejb</groupId>
      <artifactId>tomee-embedded</artifactId>
      <version>1.7.2</version><!--1.7.2-->
      <scope>test</scope>
  </dependency>
  <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-api</artifactId>
      <version>6.0</version>
  </dependency>
</dependencies>


Обратите внимание на последовательность зависимостей org.apache.openejb:tomee-embedded и javax:javaee-api. Если поменять их местами, то вместо javax:javaee-api будут использованы классы из org.apache.openejb:javaee-api и проект не соберётся.
В качестве сущности возьмём класс Event.java, описывающий некоторое запланированное событие или “напоминалку”, которую обычно заносят в ежедневник.

package com.tuneit.tomcatejbtest;

import java.io.Serializable;
import javax.persistence.*;
import java.util.Date;

@Entity
public class Event implements Serializable {

  @Id
  @GeneratedValue(strategy = GenerationType.TABLE)
  private Long id;
  private String title;
  private String description;

  @Temporal(javax.persistence.TemporalType.TIMESTAMP)
  private Date eventDate;

  private boolean done = false;
  private boolean deleted = false;

  public Event() { }
 
  public Event(Date eventDate, String title, String description) {
      this.eventDate = eventDate;
      this.title = title;
      this.description = description;
  }
 
  public Long getId() { return id; }
  public void setId(Long id) { this.id = id; }

  public String getTitle() { return title; }
  public void setTitle(String title) { this.title = title; }

  public String getDescription() { return description; }
  public void setDescription(String description) { this.description = description; }

  public Date getEventDate() { return eventDate; }
  public void setEventDate(Date eventDate) { this.eventDate = eventDate; }
 
  public boolean isDone() { return done; }
  public void setDone(boolean done) { this.done = done; }

  public boolean isDeleted() { return deleted; }
  public void setDeleted(boolean deleted) { this.deleted = deleted; }
}


Интерфейс, используемый в EJB для доступа к методам бина - EventsServiceLocal.java.

package com.tuneit.tomcatejbtest;

import java.util.Date;
import javax.ejb.Local;
import java.util.List;

@Local
public interface EventServiceLocal {
  void createOrUpdate(Event entity);
  List<Event> getAll();
  public List<Event> getAllByDate(Date date);
  Event find(Object id);
  void delete(Event entity);
}


Бин для работы с сущностью - EventsService.java.

package com.tuneit.tomcatejbtest;

import java.text.SimpleDateFormat;
import java.util.Date;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
import javax.persistence.TemporalType;

@Stateless
public class EventService implements EventServiceLocal {

  @PersistenceContext(unitName = "com.tuneit.event.PU")
  private EntityManager entityManager;
  private SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy");

  public EntityManager getEntityManager() {
    return this.entityManager;
  }

  @Override
  public void createOrUpdate(Event entity) {
    if (entity.getId() == null) {
      getEntityManager().persist(entity);
    } else {
      getEntityManager().merge(entity);
    }
  }

  @Override
  public Event find(Object id) {
    return entityManager.find(Event.class, id);
  }

  @Override
  public List<Event> getAll() {
    return entityManager.createQuery("select e from Event e where e.deleted = false").getResultList();
  }
 
  @Override
  public List<Event> getAllByDate(Date date) {
    return entityManager.createQuery("select e from Event e where to_char(e.eventDate, 'DD.MM.YYYY') = :eventDate and e.deleted = false")
            .setParameter("eventDate", dateFormat.format(date)).getResultList();
  }
 
  @Override
  public void delete(Event entity){
    entityManager.remove(entity);
  }
}


Создадим пакет с тестами, содержащий один тестовый класс и один тестовый бин. Так как бин используется только в тестах, его мы тоже помещаем в тестовый пакет.
Интерфейс тестового бина - HSQLServiceLocal.java.

package com.tuneit.test;

import javax.ejb.Local;

@Local
public interface HSQLServiceLocal {
  public void clearDatabase();
}


Тестовый бин для очистки базы данных - HSQLService.java.

package com.tuneit.test;

import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@Stateless
public class HSQLService implements HSQLServiceLocal {  
  @PersistenceContext(unitName = "com.tuneit.event.PU")
  private EntityManager entityManager;
 
  public void clearDatabase() {
    entityManager.createNativeQuery("TRUNCATE SCHEMA PUBLIC RESTART IDENTITY AND COMMIT NO CHECK").executeUpdate();
  }
}


Тестовый класс EventsTest.java.

package com.tuneit.test;

import com.tuneit.tomcatejbtest.Event;
import java.util.Date;
import java.util.Properties;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.junit.BeforeClass;
import org.junit.Test;
import com.tuneit.tomcatejbtest.EventServiceLocal;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import static org.junit.Assert.*;

public class EventsTest {

  private static InitialContext initialContext;
  EventServiceLocal eventService = getService(EventServiceLocal.class);
  
  @BeforeClass
  public static void init() {
    Properties p = new Properties();
    p.put(Context.INITIAL_CONTEXT_FACTORY, "org.openejb.client.LocalInitialContextFactory");

    p.put("jdbc/DBPool", "new://Resource?type=DataSource");
    p.put("jdbc/DBPool.JdbcDriver", "org.hsqldb.jdbcDriver");
    p.put("jdbc/DBPool.JdbcUrl", "jdbc:hsqldb:file:mem:mydb");
    p.put("jdbc/DBPool.JtaManaged", "true");
    p.put("openejb.jndiname.format", "{interfaceClass}");

    try {
      InitialContext ctx = initialContext = new InitialContext(p);
    } catch (NamingException ex) {
      ex.printStackTrace();
    }
  }
  
  @Test
  public void policyTest() {
    HSQLServiceLocal hs = getService(HSQLServiceLocal.class);
    hs.clearDatabase();
    
    SimpleDateFormat dtf = new SimpleDateFormat("dd.MM.yyyy hh:mm");

    try {
      Date day = dtf.parse("16.11.2016 12:00");
      eventService.createOrUpdate(new Event(dtf.parse("16.11.2016 17:00"), "День рождения кота", ""));
      eventService.createOrUpdate(new Event(dtf.parse("16.11.2016 13:00"), "Пальто!", "Забрать из чистки пальто"));
      eventService.createOrUpdate(new Event(dtf.parse("20.11.2016 10:00"), "Scopus", "Подать заявку на конференцию"));
      eventService.createOrUpdate(new Event(dtf.parse("16.11.2016 09:00"), "Подарок", "Заказать мешок корма"));
      eventService.createOrUpdate(new Event(dtf.parse("13.11.2016 19:00"), "Театр", "Спектакль 'Элиза', партер"));
      
      assertEquals(5, eventService.getAll().size());
      assertEquals(3, eventService.getAllByDate(day).size());
    } catch (ParseException ex) { fail(); }
  }
  
  public <T> T getService(Class<T> clazz) {
    T result = null;

    try {
      result = (T) initialContext.lookup(clazz.getCanonicalName());
    } catch (NamingException e) {
      result = null;
    }
    return result;
  }
}


В данном примере для хранения данных во время тестирования используется HSQLDB.База целиком хранится в оперативной памяти, что увеличивает скорость выполнения тестов. Однако в отладочных целях её можно поместить в файл, изменив значение параметра JdbcUrl:
p.put("jdbc/DBPool.JdbcUrl", "jdbc:hsqldb:file:/tmp/mydb.sql;hsqldb.write_delay=false");
При хранении БД в файле, имеет смысл очищать данные перед каждым запуском тестов, чтобы исключить влияние предыдущих запусков. Такого же эффекта можно добиться разными способами, но мы для примера будем использовать бин, чтобы показать, что EJB могут быть использованы и для исключительно тестовых целей.
Название пула может быть любым. Параметр hsqldb.write_delay позволяет видеть все изменения БД в файле. Если его не установить, кэш базы может быть не сброшен в процессе выполнения теста, и часть значений вообще не попадёт в файл.
Инициализировать контейнер можно двумя способами:

  • LocalInitialContextFactory
  • EJBContainer.createEJBContainer()

Последний, более новый, доступен с EJB 3.1.
Итоговая структура проекта показана ниже.

Собираем проект и наблюдаем в выводе результат выполнения теста.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running com.tuneit.test.EventsTest
INFO - ********************************************************************************
INFO - OpenEJB http://tomee.apache.org/
INFO - Startup: Wed Nov 16 17:59:14 MSK 2016
INFO - Copyright 1999-2013 (C) Apache OpenEJB Project, All Rights Reserved.
INFO - Version: 4.7.2
INFO - Build date: 20150517
INFO - Build time: 10:10
INFO - ********************************************************************************
INFO - openejb.home = /home/lexa/NetBeansProjects/TomcatEJBTest
INFO - openejb.base = /home/lexa/NetBeansProjects/TomcatEJBTest
INFO - Created new singletonService org.apache.openejb.cdi.ThreadSingletonServiceImpl@5ff8813b
INFO - Succeeded in installing singleton service
INFO - Cannot find the configuration file [conf/openejb.xml].  Will attempt to create one for the beans deployed.
INFO - Configuring Service(id=Default Security Service, type=SecurityService, provider-id=Default Security Service)
INFO - Configuring Service(id=Default Transaction Manager, type=TransactionManager, provider-id=Default Transaction Manager)
INFO - Configuring Service(id=jdbc/DBPool, type=Resource, provider-id=Default JDBC Database)
INFO - Found EjbModule in classpath: /home/lexa/NetBeansProjects/TomcatEJBTest/target/classes
INFO - Found EjbModule in classpath: /home/lexa/NetBeansProjects/TomcatEJBTest/target/test-classes
INFO - Beginning load: /home/lexa/NetBeansProjects/TomcatEJBTest/target/classes
INFO - Beginning load: /home/lexa/NetBeansProjects/TomcatEJBTest/target/test-classes
INFO - Configuring enterprise application: /home/lexa/NetBeansProjects/TomcatEJBTest/classpath.ear
INFO - Auto-deploying ejb EventService: EjbDeployment(deployment-id=EventService)
INFO - Auto-deploying ejb HSQLService: EjbDeployment(deployment-id=HSQLService)
INFO - Configuring Service(id=Default Stateless Container, type=Container, provider-id=Default Stateless Container)
INFO - Auto-creating a container for bean EventService: Container(type=STATELESS, id=Default Stateless Container)
INFO - Configuring PersistenceUnit(name=com.tuneit.event.PU, provider=org.hibernate.ejb.HibernatePersistence)
INFO - Auto-creating a Resource with id 'jdbc/DBPoolNonJta' of type 'DataSource for 'com.tuneit.event.PU'.
INFO - Configuring Service(id=jdbc/DBPoolNonJta, type=Resource, provider-id=jdbc/DBPool)
INFO - Adjusting PersistenceUnit com.tuneit.event.PU <non-jta-data-source> to Resource ID 'jdbc/DBPoolNonJta' from 'null'
INFO - Enterprise application "/home/lexa/NetBeansProjects/TomcatEJBTest/classpath.ear" loaded.
INFO - Creating TransactionManager(id=Default Transaction Manager)
INFO - Creating SecurityService(id=Default Security Service)
INFO - Creating Resource(id=jdbc/DBPool)
INFO - Creating Resource(id=jdbc/DBPoolNonJta)
INFO - Creating Container(id=Default Stateless Container)
INFO - Assembling app: /home/lexa/NetBeansProjects/TomcatEJBTest/classpath.ear
WARN - HHH015016: Encountered a deprecated javax.persistence.spi.PersistenceProvider [org.hibernate.ejb.HibernatePersistence]; use [org.hibernate.jpa.HibernatePersistenceProvider] instead.
INFO - HHH000204: Processing PersistenceUnitInfo [
    name: com.tuneit.event.PU
    ...]
INFO - HHH000412: Hibernate Core {5.0.2.Final}
INFO - HHH000206: hibernate.properties not found
INFO - HHH000021: Bytecode provider name : javassist
INFO - HCANN000001: Hibernate Commons Annotations {5.0.0.Final}
INFO - HHH000400: Using dialect: org.hibernate.dialect.HSQLDialect
INFO - HHH000398: Explicit segment value for id generator [hibernate_sequences.sequence_name] suggested; using default [default]
INFO - HHH000228: Running hbm2ddl schema update
INFO - HHH000262: Table not found: Event
INFO - HHH000262: Table not found: Event
INFO - HHH000262: Table not found: hibernate_sequences
INFO - HHH000262: Table not found: hibernate_sequences
INFO - HHH000262: Table not found: Event
INFO - HHH000262: Table not found: hibernate_sequences
INFO - PersistenceUnit(name=com.tuneit.event.PU, provider=org.hibernate.ejb.HibernatePersistence) - provider time 2762ms
INFO - Using 'openejb.jndiname.format={interfaceClass}'
INFO - Jndi(name=com.tuneit.tomcatejbtest.EventServiceLocal) --> Ejb(deployment-id=EventService)
INFO - Jndi(name=global/classpath.ear/TomcatEJBTest/EventService!com.tuneit.tomcatejbtest.EventServiceLocal) --> Ejb(deployment-id=EventService)
INFO - Jndi(name=global/classpath.ear/TomcatEJBTest/EventService) --> Ejb(deployment-id=EventService)
INFO - Using 'openejb.jndiname.format={interfaceClass}'
INFO - Jndi(name=com.tuneit.test.HSQLServiceLocal) --> Ejb(deployment-id=HSQLService)
INFO - Jndi(name=global/classpath.ear/TomcatEJBTest/HSQLService!com.tuneit.test.HSQLServiceLocal) --> Ejb(deployment-id=HSQLService)
INFO - Jndi(name=global/classpath.ear/TomcatEJBTest/HSQLService) --> Ejb(deployment-id=HSQLService)
INFO - Existing thread singleton service in SystemInstance(): org.apache.openejb.cdi.ThreadSingletonServiceImpl@5ff8813b
INFO - OpenWebBeans Container is starting...
INFO - Adding OpenWebBeansPlugin : [CdiPlugin]
INFO - Adding OpenWebBeansPlugin : [OpenWebBeansJsfPlugin]
INFO - All injection points were validated successfully.
INFO - OpenWebBeans Container has started, it took 136 ms.
INFO - Created Ejb(deployment-id=EventService, ejb-name=EventService, container=Default Stateless Container)
INFO - Created Ejb(deployment-id=HSQLService, ejb-name=HSQLService, container=Default Stateless Container)
INFO - Started Ejb(deployment-id=EventService, ejb-name=EventService, container=Default Stateless Container)
INFO - Started Ejb(deployment-id=HSQLService, ejb-name=HSQLService, container=Default Stateless Container)
INFO - Deployed Application(path=/home/lexa/NetBeansProjects/TomcatEJBTest/classpath.ear)
INFO - HHH000397: Using ASTQueryTranslatorFactory
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.886 sec

Results :
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

 

Полезные ссылки:

  1. Apache TomEE | Embedded Configuration
  2. Apache TomEE | JPA Hibernate Example
  3. Apache TomEE | Documentation