wtorek, 18 marca 2008

TestNG - pierwsze kroki

Przy okazji pisania przykładu do artykułu "JPA - pierwsze kroki" postanowiłem, że zrobię też pierwsze kroki do poznania framework'u wspomagającego wykonywanie testów jednostkowych - TestNG. Moje pierwsze wrażenie jest bardzo pozytywne. W porównaniu do jUnit'a ma prostrzy i bardziej przystępny interfejs, przy czym ma większe możliwości i jest też dużo lepiej udokumentowany.

Hierarchia testów

W TestNG mamy doczycznienia z następującą hierarchią testów: Suite (zestaw), Test, Class (klasa zawierająca metody testujące) oraz Method (metoda testująca). O ile Suite i Test są jednostkami czysto logicznymi (definiowane na poziomie konfiguracji), to Class i Method są jednostkami zawierającymi fizyczną implementację testów. Dodatkowo TestNG umożliwia znakowania Class i Method znacznikiem grupy. Przy czym istnieje możliwość przypisania ich do wielu grup. Dla wszystkich wspomnianych jednostek hierarchii oraz dla grup można określać metody, które będą wywoływane przed lub po wykonaniu testów.

Zależności między testami

TestNG umożliwia definiowanie zależności między testami. Można ustalić, że wykonanie danej metoda testująca lub klasy z testami będzie zależne od pozytywnego wykonania innych metod testujących bądź grup testów.

Konfiguracja testów

Testy w TestNG konfiguruje się w pliku xml zgodnym z DTD: http://testng.org/testng-1.0.dtd oraz poprzez adnotacje. W jednym pliku konfiguracyjnym można zdefiniować jeden Suite (znacznik <suite>), w obrębie którego definiowana jest dowolna liczba Test'ów (znaczniki <test>), lub można zaimportować pliki konfiguracyjne innych zestawów testów. Na Test z kolei może się składać dowolna liczba grup (znacznik <groups>), klas (znacznik <classes>), a także można określać pakiety Javy (znacznik <pakages>), w obrębie których będą wyszukiwane klasy do testowanie. Dla klasy można dodatkowo określić, które metody mają być wykonane, a które nie. Zarówno dla jednostek Suite jak i Test można definiować parametry, które mogą być przekazane metodom testującym (znacznik <parameter>). Z użyciem adnotacje można m.in. określić takie rzeczy jak: - czy klasa zawiera testy (adnotacja Test), - czy dana metoda jest metodą testową (również adnotacja Test), - określenie grupy do której przynależy klasa bądź metoda (parametr groups do adnotacji Test), - czy metoda testująca bądź klasa zawierająca testy jest zależna od innych testów (parametr dependsOnMethods oraz dependsOnGroups adnotacji Test), - określenie metod do wywołania przed lub po wykonaniu testów (adnotacje Before* oraz After*), - określenie parametrów do przekazania metodzie testującej (adnotacja Parameters)

Przykład

Poniższy przykład to testy jednostkowe przygotowane na potrzeby artykułu "JPA - pierwsze kroki". Składa się na niego plik konfiguracyjny, abstrakcyjna klasa bazowa oraz dwie klasy konkretne zawierające metody testujące.
Plik konfiguracyjny
Plik konfiguracyjny zawiera oczywiście definicję testu (implementowany w dwóch klasach), a także wykorzystywanych przez większość metod testujących parametry.
<suite name="Suite1">
 <parameter name="user1" value="heniek" />
 <parameter name="user2" value="franek" />
 
 <test name="Test">
  <classes>
   <class name="pl.dwalczak.jpapg1.UserTest" />
   <class name="pl.dwalczak.jpapg1.MessageTest" />
  </classes>
 </test>
</suite>
Abstrakcyjna klasa bazowa
Ta klasa definiuje podstawowe usługi potrzebne przy testowaniu mapowania modelu JPA. Przede wszystkim dostarcza EntityManager, którego inicjuje i rozpoczyna transakcję przed wykonaniem każdej metody testującej. Natomiast po wykonaniu metody testującej zatwierdza transakcję oraz zamyka EntityManager'a. Po wykonaniu całego zestawu testów (Suite) zamykany jest EntityManagerFactory.
package pl.dwalczak.jpapg1;
import javax.persistence.EntityManager;
import org.apache.log4j.Logger;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.AfterSuite;
import org.testng.annotations.BeforeMethod;
import pl.dwalczak.jpapg1.util.JpaUtil;

abstract public class JpaTestCase {
 
 private static final Logger LOG = Logger.getLogger(JpaTestCase.class);
 protected EntityManager em;

 @BeforeMethod
 public void beginWorkUnitAndTransaction() {
  try {
   em = JpaUtil.getEntityManager();
   em.getTransaction().begin();
  } catch (RuntimeException e) {
   LOG.error("Can't beginWorkUnitAndTransaction", e);
  }
 }
 
 @AfterMethod
 public void closeWotkUnitAndCommitTransaction() {
  try {
   if (false == em.getTransaction().getRollbackOnly()) {
    em.getTransaction().commit();
   } else {
    em.getTransaction().rollback();
   }
  } catch (RuntimeException e) {
   LOG.error("Can't closeWotkUnitAndCommitTransaction", e);
  } finally {
   if (null != em) {
    try {
     if (em.getTransaction().isActive()) {
      em.getTransaction().rollback();
     }
     em.close();
    } catch (RuntimeException e) {
     LOG.error("Can't close entity manager!", e);
    }
   }
   em = null;
  }
 }
 
 @AfterSuite
 public void closeEntityManagerFactory() {
  JpaUtil.closeEntityManagerFactory();
 }
}
Klasa UserTest
Klasa ta zawiera wszystkie testy dotyczące encji "Użytkownik" i jest opatrzona w odpowiedni znacznik grupy ("user"). Znajdują się w niej przykłady przekazywania parametrów do metod testujących oraz określania zależności między metodami testującymi.
package pl.dwalczak.jpapg1;
import java.util.List;
import org.apache.log4j.Logger;
import org.testng.Assert;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
import pl.dwalczak.jpapg1.model.User;

@Test(groups="user")
public class UserTest extends JpaTestCase{
 
 private static final Logger LOG = Logger.getLogger(UserTest.class);

 @Test
 @Parameters({ "user1" })
 public void createUser1(String user) {
  LOG.debug("create user: " + user);
  em.persist(new User(user));
 }
 
 @Test
 @Parameters({ "user2" })
 public void createUser2(String user) {
  LOG.debug("create user: " + user);
  em.persist(new User(user));
 }
 
 @Test(dependsOnMethods={"createUser1", "createUser2"})
 @SuppressWarnings(value="unchecked")
 public void list() {
  LOG.debug("get user list");
  em.getTransaction().setRollbackOnly();
  List list = em.createNamedQuery("user.findAll").getResultList();
  LOG.debug("user list: " + list);
  Assert.assertEquals(list.size(), 2);
 }
 
 @Test(dependsOnMethods={"createUser2"})
 @Parameters({ "user2" })
 public void findUser(String user) {
  LOG.debug("find user: " + user);
  User u = (User) em.createNamedQuery("user.findByNickName").setParameter("nickName", user).getSingleResult();
  LOG.debug("found user: " + u);
  Assert.assertNotNull(u);
  
 }
}
Klasa MessageTest
Klasa ta zawiera wszystkie testy dotyczące encji "Wiadomość". Wykonanie wszystkich metod testujących tej klasy jest uzależnione od pozytywnego wyniku testów z grupy "user".
package pl.dwalczak.jpapg1;
import java.util.List;
import org.apache.log4j.Logger;
import org.testng.Assert;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;
import pl.dwalczak.jpapg1.model.Message;
import pl.dwalczak.jpapg1.model.User;

@Test(dependsOnGroups="user")
public class MessageTest extends JpaTestCase {
 
 private static final Logger LOG = Logger.getLogger(MessageTest.class);
 
 @Test
 @Parameters({ "user1", "user2" })
 public void user1SendToUser2(String user1, String user2) {
  LOG.debug("user1SendToUser2");
  sendMessage("title1", "content1", user1, user2);
 }
 
 @Test
 @Parameters({ "user2", "user1" })
 public void user2SendToUser1(String user1, String user2) {
  LOG.debug("user2SendToUser1");
  sendMessage("title2", "content2", user1, user2);
 }
 
 private void sendMessage(String title, String content, String userFrom, String userTo) {
  User uFrom = (User) em.createNamedQuery("user.findByNickName").setParameter("nickName", userFrom).getSingleResult();
  User uTo = (User) em.createNamedQuery("user.findByNickName").setParameter("nickName", userTo).getSingleResult();
  Message message = new Message(title, content, uFrom, uTo);
  LOG.debug("send a message: " + message);
  em.persist(message);
 }
 
 @Test(dependsOnMethods={"user1SendToUser2", "user2SendToUser1"})
 @Parameters({"user1"})
 @SuppressWarnings(value="unchecked")
 public void viewUser1Messages(String user) {
  LOG.debug("view messages of: " + user);
  User u = (User) em.createNamedQuery("user.findByNickName").setParameter("nickName", user).getSingleResult();
  Assert.assertNotNull(u);
  List received = em.createNamedQuery("message.findReceived").setParameter("user", u).getResultList();
  LOG.debug("received messages: " + received);
  Assert.assertEquals(received.size(), 1);
  List send = em.createNamedQuery("message.findSend").setParameter("user", u).getResultList();
  LOG.debug("send messages: " + send);
  Assert.assertEquals(send.size(), 1);
 }
}

Zasoby

Pliki źródłowe
Dokumentacja TestNG

Brak komentarzy: