Testowanie DAO w JPA 2.0 za pomocą DbUnit część 2

Malowanie zakończone sukcesem. Zatem czas na druga część problemu testowania klas DAO z użyciem DBUnit w środowisku z JPA 2.0. Bogowie, co za tytuł… ja powinienem z tego tytuł magisterki zrobić. Swoją drogą farba „Willow Creek 4” ma taki ładny kolor… jak przechodzące zestawy testów.

Do rzeczy. W poprzedniej części postawiłem warunki jakie powinien spełniać test bazy danych. Przyjrzyjmy się zatem co chcemy testować.

Listing 1. CrudDao

public interface CrudDao {

	void delete(T t);

	T getById(Object id);

	T saveOrUpdate(T t);
}

Interfejs CrudDao składa się z trzech metod. Więcej nam nie trzeba choć można by było dodać metody odpowiedzialne za np. pobranie wszystkich encji czy wyliczenie ile encji jest w bazie. Tyle tylko, że nie zawsze potrzebujemy taką funkcjonalność, a konieczność jej implementacji w każdym Dao to tworzenie dodatkowego, zbędnego kodu. Jeżeli chcemy dodać jakaś funkcjonalność zawsze możemy dodać ją do interfejsu Dao dla konkretnej klasy.

Listing 2. QuestionDao

public interface QuestionDao extends CrudDao {
	
	Collection getAll();
	
}

Na listingu 2 mamy właśnie przykład takiego rozszerzenia o dodatkową funkcjonalność.

Realizacja założeń dla testu

Pierwsze założenie mówi nam, że chcemy testować tylko jedną rzecz. Testujemy zatem interfejs QuestionDao. Właśnie interfejs, a nie implementację. Oznacza to, że test powinien być w jakimś stopniu konfigurowalny w zakresie wymiany implementacji. Można osiągnąć to na kilka sposobów. Moim ulubionym jest oznaczenie klasy z testami jako abstrakcyjnej i wymuszenie na dostawcy implementacji przygotowania prostego rozszerzenia klasy testującej, które to rozszerzenie będzie dostarczać implementacji. Innym podejściem jest użycie np. Guice w celu wstrzyknięcia zależności. Powiem szczerze, że nie lubię tej metody. Ogranicza nas i utrudnia stworzenie wielu implementacji w ramach jednego projektu.
Szablon klasy testowej znajduje się na listingu 3.

Listing 3. Szablon QuestionDaoTest

public abstract class QuestionDaoTest {

	private QuestionDao questionDao;
	
	private DataSource dataSource;

	private IDataSet testDataSet;

	@Test
	public void testById() throws Exception {}

	@Test
	public void testDelete() throws Exception {}

	@Test
	public void testGetAll() throws Exception {}

	@Test
	public void testSaveOrUpdate() throws SQLException, Exception {}

	protected abstract void closeTest();

	protected abstract QuestionDao getQuestionDao();

	@BeforeMethod
	protected void setUpTest() throws Exception {
		questionDao = getQuestionDao();
		dataSource = getDataSource();
	}

	@AfterMethod
	protected void tearDownTest() throws Exception {
		closeTest();
		questionDao = null;
		cleanUp(testDataSet);
		dataSource.getConnection().close();
	};

	private void loadToDatabase() throws Exception {
		testDataSet = getIDataSetFromXml("question-get-test.xml");
		cleanInsert(testDataSet);
	}
	
	public static void assertQuestions(Question actual, Question expected) {
		assertEquals(actual.getId(), expected.getId(), "Questions id not equal");
		assertEquals(actual.getTitle(), expected.getTitle(),
				"Questions title not equal");
		assertEquals(actual.getDescription(), expected.getDescription(),
				"Questions description not equal");
	}
}

Jak łatwo zauważyć pomysł na testy jest bardzo prosty. Jako, że musimy uwzględnić punktu drugi z listy wymagań czyli niezależność zatem wykorzystujemy adnotacje @BeforeMethod i @AfterMethod zamiast @BeforeClass i @AfterClass. Oczywiście adnotacje @XClass mogą zostać użyte jeżeli mamy taką potrzebę. Przy czym to co inicjujemy to powinny być mocki, a nie rzeczywiste klasy! Testujemy Dao, a nie serwisy czy narzędzia!
Kluczową funkcjonalność naszych testów dostarczają dwie abstrakcyjne metody getQuestionDao i closeTest. Pierwsza z nich dostarcza implementacji Dao. Implementacji gotowej do użycia, czyli takiej w której znajdują się już wszystkie zależności. Z punktu widzenia testu nie jest istotne czy implementacja używa JPA czy JDBC czy pracuje z XMLem za pomocą XPath. Rolą dostawcy implementacji jest też dostarczenie w pełni funkcjonalnej instancji do testów. Druga metoda wraz z metodą tearDownTest służy do zaspokojenia naszych potrzeb w zakresie trzeciego punktu. Ogólna zasada jest taka, że wszystko co zostało stworzone w getQuestionDao tu zostaje zamknięte. Tu też zostaje wyczyszczona baza danych. Kolejność zamykania zasobów może być dowolna. Warto jednak pamiętać, że jeżeli używamy JPA to konfiguracja close-drop powoduje, że EntityManagerFactory powinno być zamykane jako ostatnie. Inaczej DBUnit zgłosi błąd braku tabeli (usuniętej przy zamykaniu EMF).
Ostatni punkt jest trudny do spełnienia. Jeżeli chcemy by nasze testy były w miarę szybkie musimy zrezygnować z dropowania tabel po każdym teście. Z drugiej strony może się tak zdarzyć, że nie wyczyścimy wszystkiego jak należy i kolejne testy nam padną… Coś za coś. Dla mnie osobiście najwygodniejszą opcją jest czyszczenie bazy bez dropowania.

Implementacja testu na przykładzie operacji delete

W typowym środowisku w którym poszczególne testy są uzależnione od siebie test kasowania danych jest wykonywany jako ostatni. Wynika to z faktu, iż test ten wymaga jakiś danych by móc je wykasować. Jeżeli jednak korzystamy z DbUnit możemy przygotować sobie środowisko testowe niezależnie od tego co działo się przed i co będzie dziać się po teście.
Pierwszym krokiem jest przygotowanie wyjściowego zestawu danych. W tym celu użyjemy pliku XML:

Listing 4. Szablon question-get-test.xml



	
	
	
	
	
	
	
	
	

DbUnit odczyta ten plik i na podstawie nazw elementów i atrybutów XML wstawi do bazy odpowiednie rekordy. Kolejny krok to przygotowanie zestawu wyników. W teście będziemy usuwać rekord z tabeli Question o questionid równym 1. W wyniku tej operacji chcemy uzyskać zestaw danych taki jak w pliku question-after-delete-test.xml

Listing 5. Szablon question-after-delete-test.xml



	
	
	
	

Test powinien zatem:

  • załadować plik z danymi do bazy
  • wywołać metodę delete z obiektem Question o questionid równym 1
  • Załadować plik ze spodziewanym stanem bazy
  • Sprawdzić zawartość tabeli Vote
  • Sprawdzić zawartość tabeli Question
  • Na koniec wywołać metodę delete z parametrem null oraz jakimś obiektem, który nie istnieje w bazie by zweryfikować zachowanie w takim przypadku.

Mając tak przygotowane wymagania można spokojnie napisać test:

Listing 6. Test weryfikujący metodę delete

@Test                                                                  
public void testDelete() throws Exception {                            
	loadToDatabase();                                                  
	questionDao.delete(new Question(1L, "", "", null));                
	testDataSet = getIDataSetFromXml("question-after-delete-test.xml");
	                                                                   
	ITable expectedTableVoteAfterDelete = testDataSet.getTable("vote");
	ITable actualTableVote = getTableFromDatabase(dataSource, "vote"); 
	assertEquals(actualTableVote.getRowCount(), expectedTableVoteAfterDelete.getRowCount());               
	                                                                   
	ITable expectedTableQuestionAfterDelete = testDataSet.getTable("question");                                     
	ITable actualTableQuestion = getTableFromDatabase(dataSource, "question");                                               
	assertEquals(actualTableQuestion.getRowCount(), expectedTableQuestionAfterDelete.getRowCount());           
                                                                       
	// row with questionid=1 not exist.                                
	for (int i = 0; i < actualTableQuestion.getRowCount(); i++)        
		assertNotEquals(actualTableQuestion.getValue(i, "questionid"),  new Long(1L));		                                   
                                                                       
	questionDao.delete(null);                                          
	questionDao.delete(new Question(9L, "", "", null));                
}                                                                      

Na początek ładujemy dane do bazy. Następnie wywołujemy metodę delete i weryfikujemy stan bazy ze spodziewanym. Na koniec dwa kontrolne wywołania dla nieistniejących encji.

Tak samo implementujemy kolejne testy.

Implementacja konkretnej klasy testowej - problem podwójnej konfiguracji

Na koniec pozostaje nam dostarczenie implementacji konkretnej klasy testowej. Nie jest to trudne musimy jednak zauważyć jedną rzecz. Jeżeli chcemy wykorzystywać JPA to musimy przygotować plik persistence.xml względnie dostarczać mapę z danymi dla implementacji JPA. Powoduje to, że mamy dwie konfiguracje. W dodatku nie można wykorzystać bazy HSQLDB w trybie memory... Oznacza to, że musimy trzymać jakąś uruchomioną bazę. Jest to pewna niedogodność, z którą trzeba jednak żyć.

Listing 7. Implementacja konkretnej klasy QuestionDaoTestImpl

@Test
public class QuestionDaoImplTest extends QuestionDaoTest {

	private EntityManagerTestUtils entityManagerTestUtils;
	private EntityManager entityManager;

	@Override
	protected void closeTest() {
		entityManager.close();
	}

	@Override
	protected QuestionDao getQuestionDao() {

		entityManager = entityManagerTestUtils.getEntityManager();
		QuestionDao questionDao = new QuestionDaoImpl();
		((AbstractDao) questionDao).setEm(entityManager);
		return questionDao;
	}

	@BeforeClass
	protected void setUp() {
		entityManagerTestUtils = new EntityManagerTestUtils("onaczyon-test-pu");
	}

	@AfterClass
	protected void tearDown() {
		entityManagerTestUtils.close();
	}

}

Nie ma tu nic ciekawego. W pliku persistence.xml używam create. Całość w skonfigurowana do pracy z Postgresem. To wszystko.

Pytania?

4 myśli na temat “Testowanie DAO w JPA 2.0 za pomocą DbUnit część 2

  1. Ja mam pytanie, trochę późno, ale mam nadzieję, że masz powiadomienia. 🙂

    Mógłbyś wysłać na maila całkowity kod źródłowy przykładu? Ten wydaje się niekompletny, tzn. nie ma np. metody getTableFromDatabase() czy getIDataSetFromXml(). Nie widać tutaj, żebyś po czymś dziedziczył, a nie ma też implementacji tych metod. Dopiero się uczę DBUnit i taki kod byłby bardzo pomocny. 🙂

  2. Metody te przesłaniają standardowe mechanizmy DBUnita. Dzięki za info, bo swoja droga mam takie rozszerzenie do testów gotowe już, ale jakoś nie przyszło mi do głowy publikować (bo po co to komu).

  3. Mógłbyś tylko napisać, co robią metody closeTest(); oraz cleanUp(testDataSet);? Ciągle nie mogę zmusić DBUnit to poprawnego działania – pierwszy test (insert) wykonuje się poprawnie, w drugim leci błąd „java.sql.SQLException: user lacks privilege or object not found: (chyba losowa tabela z bazy danych)”. Może to ich implementacji mi brakuje. 🙂

    Na razie robię to tak, że stawiam HSQLDB, w setUp() ładuję za pomocą CLEAN_INSERT dataset i potem przeprowadzam testy.. Jeżeli masz jakieś uwagi co do tego, to mile widziane.

  4. Mea culpa, closeTest(); jest pokazany. 🙂 cleanUp(); jednak nie..

    I przy okazji – do testów wykorzystuję, jak w całej aplikacji, Springa i Hibernate’a. Wiesz może jak tutaj zapewnić niezależność testów?

    Cały czas się głowię, skąd tamte wyjątki i coś mi się zdaje, że mogą one być właśnie z tego..

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax