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<t> {

	void delete(T t);

	T getById(Object id);

	T saveOrUpdate(T 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<question> {
	
	Collection<question> getAll();
	
}</question></question>

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

<?xml version="1.0" encoding="UTF-8"??><dataset><question description="opis" questionid="1" title="Ona czy on?"></question><question description="opis" questionid="2" title="Ona czy on?"></question><question description="opis" questionid="3" title="Ona czy on?"></question><vote address="0.0.0.0" mode="HER" questionid="1" voteid="1"></vote><vote address="0.0.0.0" mode="HER" questionid="1" voteid="2"></vote><vote address="0.0.0.0" mode="HER" questionid="1" voteid="3"></vote><vote address="0.0.0.0" mode="HIM" questionid="1" voteid="4"></vote><vote address="0.0.0.0" mode="HIM" questionid="1" voteid="5"></vote></dataset>

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

<?xml version="1.0" encoding="UTF-8"??><dataset><question description="opis" questionid="2" title="Ona czy on?"></question><question description="opis" questionid="3" title="Ona czy on?"></question><vote></vote></dataset>

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 
<p>Na początek ładujemy dane do bazy. Następnie wywołujemy metodę <samp>delete</samp> i weryfikujemy stan bazy ze spodziewanym. Na koniec dwa kontrolne wywołania dla nieistniejących encji.</p>
<p>Tak samo implementujemy kolejne testy.</p>
<h4>Implementacja konkretnej klasy testowej - problem podwójnej konfiguracji</h4>
<p>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 <samp>persistence.xml</samp> 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ć.</p>
<p class="listing">Listing 7. Implementacja konkretnej klasy <samp>QuestionDaoTestImpl</samp></p>java
@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();
	}

}
<p>Nie ma tu nic ciekawego. W pliku <samp>persistence.xml</samp> używam <samp>create</samp>. Całość w skonfigurowana do pracy z Postgresem. To wszystko.</p>
<p>Pytania?</p>