RSS

Testowanie usług wykorzystujących JPA

Liczba odsłon: 295

Testy jed­nost­ko­we to tech­ni­ka, któ­ra zmie­nia spo­sób, w ja­ki two­rzy się opro­gra­mo­wa­nie. Z kolei me­to­do­lo­gie TDD (TFD) i TAD, sto­so­wa­ne z roz­sąd­kiem, po­zwa­la­ją zna­czą­co ogra­ni­czyć czas uru­cha­mia­nia no­wych usług i zwięk­szyć nie­za­wod­ność ich dzia­ła­nia. Testowa­nie pod­pro­gra­mów i klas ope­ru­ją­cych na da­nych za­pi­sa­nych w pa­mię­ci opera­cyj­nej czy na dys­ku jest pros­te. Problem po­ja­wia się, gdy trze­ba te­sto­wać usłu­gi ko­mu­ni­ku­ją­ce się z ba­zą da­nych.

Tech­ni­ki ta­kie jak two­rze­nie imi­ta­cji klas DAO lub za­rząd­cy en­cji (ang. en­ti­ty ma­na­ger) dzia­ła­ją tyl­ko do mo­men­tu, gdy do­stęp do ba­zy da­nych jest rea­li­zo­wa­ny w bar­dzo pros­ty spo­sób. Wystar­czy jed­nak, by ko­niecz­ne by­ło reali­zo­wa­nie włas­nych za­py­tań JPQL lub SQL, a two­rze­nie imi­ta­cji sta­je się trud­ne i cza­so­chłon­ne. Poza tym, zbyt roz­bu­do­wa­na imi­tac­ja od ra­zu wpro­wa­dza wąt­pli­wo­ści: czy tes­tu­je­my im­ple­men­tac­ję ja­kiejś usłu­gi, czy też jej imi­tac­ję?

Najskutecz­niej­szym roz­wią­za­niem jest wpro­wa­dze­nie pro­ce­du­ry tes­to­wa­nia usług z wy­ko­rzys­ta­niem praw­dzi­wej ba­zy da­nych. Takie roz­wią­za­nie po­zwa­la ogra­ni­czyć do mi­ni­mum ilość ko­du tes­tu­ją­ce­go, a za­ra­zem zna­czą­co pod­nieść po­kry­cie przez te­sty rze­czy­wis­tych ście­żek reali­zac­ji usłu­gi. Nie jest ono jed­nak bez wad:

Na szczęś­cie moż­na sko­rzys­tać z nie­co lżej­sze­go roz­wią­za­nia: ba­zy da­nych dzia­ła­ją­cej w pa­mię­ci opera­cyj­nej. Tego ty­pu ba­zy nie zaw­sze są do­brze zopty­ma­li­zo­wa­ne pod ką­tem szyb­koś­ci wy­ko­ny­wa­nia skom­pli­ko­wa­nych za­py­tań, lecz nie sta­no­wi to prob­le­mu, gdyż rea­li­zo­wa­ne te­sty za­zwy­czaj wy­ma­ga­ją bar­dzo ogra­ni­czo­ne­go zbio­ru da­nych. Ważniej­sze jest, że roz­ruch ta­kiej ba­zy trwa bar­dzo krót­ko, nie wy­ma­ga ona żad­nej kon­fi­gu­rac­ji, a jej stan po­cząt­ko­wy jest za każ­dym ra­zem z de­fi­ni­cji ta­ki sam — czy­li pu­sty.

W przy­pad­ku ję­zy­ka Java jed­ną z ta­kich im­ple­men­tac­ji ba­zy da­nych jest Apache Derby. Ma ona co praw­da nie­co więk­sze moż­li­woś­ci, jed­nak do­sko­na­le na­da­je się do za­sto­so­wa­nia w tes­tach jed­nost­ko­wych i in­te­gra­cyj­nych.

Dołącze­nie Derby do pro­jek­tu

Aby sko­rzys­tać z ba­zy Apache Derby w wer­sji tes­to­wej pro­gra­mu, na­le­ży do­dać do za­leż­noś­ci pro­jek­tu Maven na­stę­pu­ją­ce dwa wpi­sy (lub ich od­po­wied­ni­ki w przy­pad­ku sto­so­wa­nia in­nych sys­te­mów bu­do­wa­nia apli­kac­ji, jak na przy­kład Gradle):

<dependency>
  <groupId>org.apache.derby</groupId>
  <artifactId>derby</artifactId>
  <version>10.12.1.1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.apache.derby</groupId>
  <artifactId>derbyclient</artifactId>
  <version>10.12.1.1</version>
  <scope>test</scope>
</dependency>

Definio­wa­nie tes­to­wej jed­nost­ki utrwa­la­nia

W dru­gim kro­ku na­le­ży zde­fi­nio­wać od­dziel­ną jed­nost­kę utrwa­la­nia (ang. per­sis­ten­ce unit), zwią­za­ną z ba­zą Derby i prze­zna­czo­ną wy­łącz­nie do te­stów jed­nost­ko­wych.

Tworzona jed­nost­ka utrwa­la­nia mo­że po­wie­lać de­kla­rac­je klas en­cji wy­stę­pu­ją­ce w in­nych jed­nost­kach. Warto jed­nak do­ko­nać roz­dzia­łu i za­sto­so­wać dwa od­dziel­ne pli­ki per­sis­ten­ce.xml: je­den pro­duk­cyj­ny nie uwzględ­nia­ją­cy no­wej jed­nost­ki utrwa­la­nia oraz dru­gi, te­sto­wy, umiesz­czo­ny w ka­ta­lo­gu źró­deł te­sto­wych. Jego za­war­tość mo­że być na­stę­pu­ją­ca:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://java.sun.com/xml/ns/persistence">
  <persistence-unit name="TestPU" transaction-type="RESOURCE_LOCAL">
  <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
  <class>………</class>
  <properties>
    <property name="eclipselink.logging.level" value="FINE" />
    <property name="eclipselink.target-database" value="DERBY" />
    <property name="javax.persistence.jdbc.driver" value="org.apache.derby.jdbc.EmbeddedDriver" />
    <property name="javax.persistence.jdbc.url" value="jdbc:derby:memory:TestDB;create=true" />
    <property name="javax.persistence.jdbc.user" value="" />
    <property name="javax.persistence.jdbc.password" value="" />
    <property name="eclipselink.logging.level" value="INFO" />
    </properties>
  </persistence-unit>
</persistence>

Tworzenie za­rząd­cy utrwa­la­nia

Autorzy apli­kac­ji Java EE mo­gą prze­nieść cię­żar two­rze­nia obiek­tów za­rząd­ców utrwa­la­nia oraz ste­ro­wa­nia trans­ak­cja­mi na kon­te­ner. Niestety, w apli­kac­jach Java SE (a te­sty JUnit są ta­ki­mi apli­kac­ja­mi) na­le­ży sa­mo­dziel­nie za­dbać o stwo­rze­nie (i znisz­cze­nie!) obiek­tów Entity­Manager­Factory oraz Entity­Manager, a tak­że otwar­cie i zam­knię­cie każ­dej tran­sak­cji (z uwzględ­nie­niem ewen­tu­al­ne­go wy­co­fa­nia tran­sak­cji w ra­zie wy­stą­pie­nia sytu­acji wy­jąt­ko­wej).

W przy­pad­ku ko­rzy­sta­nia z osno­wy JUnit, naj­wy­god­niej jest na­ka­zać jej stwo­rze­nie właś­ci­we­go za­rząd­cy utrwa­la­nia przed uru­cho­mie­niem te­stów na­le­żą­cych do da­nej kla­sy oraz znisz­cze­nie go po za­koń­cze­niu wszyst­kich te­stów. Można to osiąg­nąć za po­mo­cą me­tod sta­tycz­nych, ad­no­to­wa­nych @Before­Class oraz @After­Class:

private static final String PERSISTENCE_UNIT_NAME = "TestPU";

private static EntityManagerFactory entityManagerFactory;
private static EntityManager entityManager;

@BeforeClass
public static void beforeClass() throws Exception {
  entityManagerFactory = Persistence.createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
  entityManager = entityManagerFactory.createEntityManager();
}

@AfterClass
public static void afterClass() {
  entityManager.close();
  entityManagerFactory.close();
}

W ra­zie po­trze­by sko­rzy­sta­nia z prze­twa­rza­nia trans­ak­cyj­ne­go, moż­na sko­rzys­tać z obiek­tu Entity­Trans­action zwra­ca­ne­go przez obiekt entityManager:

EntityTransaction transaction = null;
try {
  transaction = em.getTransaction();
  transaction.begin();
  ………
  transaction.commit();
} catch (final Exception exception) {
  if (transaction != null && transaction.isActive()) {
    transaction.rollback();
  }
  throw exception;
}

W prak­ty­ce naj­wy­god­niej jest prze­rzu­cić jed­nak ten obo­wią­zek na osno­wę tes­to­wą. W JUnit moż­na to osiąg­nąć za po­mo­cą wy­tycz­nych @Rule:

@Rule
public TestRule transactionRule = new TestRule() {
  @Override
  public Statement apply(final Statement base, final Description description) {
    return new Statement() {
      @Override
      public void evaluate() throws Throwable {
        EntityTransaction transaction = null;
        try {
          transaction = entityManager.getTransaction();
          if (transaction != null) {
            transaction.begin();
          }
          base.evaluate();
          if (transaction != null && transaction.isActive()) {
            transaction.commit();
          }
        } catch (final Throwable exception) {
          if (transaction != null && transaction.isActive()) {
            transaction.rollback();
          }
          throw exception;
        }
      }
    };
  }
};

Ujemną stro­ną ta­kie­go roz­wią­za­nia bę­dzie to, że no­wa trans­akcja bę­dzie otwie­ra­na i za­my­ka­na rów­nież w przy­pad­ku te­stów, któ­re nie mu­szą ko­rzy­stać z ba­zy da­nych. Problem moż­na al­bo zigno­ro­wać (o ile ta­kich te­stów jest nie­wie­le), al­bo roz­wią­zać przez wy­dzie­le­nie od­ręb­nej kla­sy te­stów.

Najwygod­niej­sze jest stwo­rze­nie włas­nej kla­sy ba­zo­wej dla ba­zo­da­no­wych te­stów jed­nost­ko­wych. Klasa ta wy­po­sa­żo­na by­ła­by we wszyst­kie po­la i me­to­dy sta­tycz­ne wy­mie­nio­ne po­wy­żej. Ponie­waż JUnit dzie­dzi­czy ad­no­to­wa­ne po­la i me­to­dy kla­sy ba­zo­wej te­stu, w kla­sie po­chod­nej wy­star­czy zde­fi­nio­wać me­to­dy @Test reali­zu­ją­ce kon­kret­ne te­sty.

Pod­su­mo­wa­nie

Dzięki wy­ko­rzys­ta­niu ba­zy da­nych reali­zu­ją­cej wszyst­kie ope­rac­je w pa­mię­ci moż­li­we jest do­głęb­ne prze­tes­to­wa­nie two­rzo­nych usług. Tak rea­li­zo­wa­ne te­sty ma­ją cha­rak­ter po­śred­ni mię­dzy jed­nost­ko­wy­mi a in­te­gra­cyj­ny­mi, gdyż z jed­nej stro­ny uru­cha­mia się po­je­dyn­cze ope­rac­je za­opat­rzo­ne we wstęp­nie przy­go­to­wa­ne spe­cjal­nie da­ne, a z dru­giej — spraw­dza się współ­dzia­ła­nie ko­du z me­cha­niz­mem utrwa­la­nia.

Z kolei wy­ko­rzys­ta­nie moż­li­woś­ci osno­wy tes­to­wej – w tym przy­pad­ku JUnit – po­zwa­la uproś­cić kod te­stów, zmniej­szyć na­kład pra­cy po­trzeb­nej do ich przy­go­to­wa­nia i w efek­cie zwięk­szyć mo­ty­wac­ję pro­gra­mis­tów do two­rze­nia te­stów i sto­so­wa­nia me­to­do­lo­gii TFDTAD.