Testy jednostkowe w Spring 3.0

10 lutego 2011, 13:47:14, Patryk Dobrowolski « Spring 3.0.x i uszkodzone drzewo zależności | Generyczne DAO dla Hibernate »

W notce zamieszczam krótki samouczek na temat tworzenia i konfiguracji transakcyjnych testów jednostkowych z wykorzystaniem Springa 3. Wskażę krok po kroku, co należy uczynić, aby uruchomić testy jednostkowe przy użyciu Mavena. Opiszę problemy, z którymi się spotkałem samemu konfigurując pierwszy test jednostkowy w Springu z wykorzystaniem Hibernate3 i MySQL. Testować będziemy serwis, a konkretnie DAO operujące na prostej tabeli zmapowanej do klasy Uzytkownik.

Istotą transakcyjnych testów jednostkowych wykonujących operacje na bazie danych jest wycofywanie wszelkich zmian, które zostały poczynione w trakcie testów. Celem tego samouczka jest automatyzacja obsługi transakcji. W takiej sytuacji nie musimy się martwić o to, czy testy wpłyną na spójność bazy w przypadku ich przerwania w trakcie, ale także w przypadku pomyślnego zakończenia.

Załóżmy, że mamy następującą klasę mapowaną na tabelę w bazie danych:

Uzytkownik.java

package pl.jedenpies.blog.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "uzytkownicy")
public class Uzytkownik {
	
	@Id @GeneratedValue(strategy = GenerationType.AUTO)	
	private Integer id;	
	
	@Column(unique = true) 
	private String email;
	@Column	
	private String haslo;
	
	public boolean isIdUstawione() {
		return id != null;
	}
	//... Gettery i settery
}

Jak widać klasa ta jest mapowana na tabelę użytkownicy z trzema kolumnami: id, email, haslo. Do tego utworzone zostało DAO z czterema podstawowymi metodami CRUD:

UzytkownikDao.java

package pl.jedenpies.blog.dao;

import pl.jedenpies.blog.domain.Uzytkownik;

public interface UzytkownikDao {

	public Uzytkownik create(Uzytkownik uzytkownik);
	public Uzytkownik read(Integer id);
	public Uzytkownik update(Uzytkownik uzytkownik);
	public boolean delete(Uzytkownik uzytkownik);
}

Jak widać jest to tylko interfejs. Konkretna implementacja nas teraz nie interesuje, nie będę więc tego tutaj zamieszczał.

Chcemy przetestować wszystkie cztery metody. Tworzymy więc prostą klasę testową:

UzytkownikDaoTest.java
package pl.jedenpies.blog.dao;

import javax.annotation.Resource;

import org.junit.Test;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.util.Assert;

import pl.jedenpies.blog.domain.Uzytkownik;
import pl.jedenpies.blog.tests.AbstractTest;

public class UzytkownikDaoTest {

	private UzytkownikDao uzytkownikDao;
	
	@Test
	@BeforeTransaction	
	public void test1Config() {
		Assert.notNull(uzytkownikDao, "UzytkownikDao nie moze byc null");
	}
	
	@Test	
	public void test2Create() {
		Uzytkownik u = simpleUzytkownik();
		Assert.isTrue(u.getId() == null);
		Integer id = uzytkownikDao.create(u);
		Assert.notNull(id, "id nie moze byc null");		
	}
	
	@Test
	public void test3Read() {
		Uzytkownik u = simpleUzytkownik();
		Integer id = uzytkownikDao.create(u);
		Assert.notNull(id);
		u = uzytkownikDao.read(id);
		Assert.notNull(u, "uzytkownik nie moze byc null");
		Assert.hasText(u.getEmail());
		Assert.hasText(u.getHaslo());		
	}
	
	@Test
	public void test3Update() {
		final String newEmail = "newEmail@example.pl";
		Uzytkownik u = simpleUzytkownik();
		Integer id = uzytkownikDao.create(u);		
		u = uzytkownikDao.read(id);
		u.setEmail(newEmail);
		uzytkownikDao.update(u);
		u = uzytkownikDao.read(id);
		Assert.isTrue(u.getEmail().equals(newEmail));
	}
	
	@Test
	public void test4Delete() {
		Uzytkownik u = simpleUzytkownik();
		Integer id = uzytkownikDao.create(u);
		uzytkownikDao.delete(u);
		u = uzytkownikDao.read(id);
		Assert.isNull(u);
	}
	
	private Uzytkownik simpleUzytkownik() {
		Uzytkownik u = new Uzytkownik();
		u.setEmail("me@example.pl");
		u.setHaslo("tajneHaslo");
		return u;
	}
}

Jak widać, w wersji 4.x JUnit nie musimy dziedziczyć z żadnej klasy typu JTestCase, czy z odpowiadającym jej klasom ze Springa, jak to dawniej bywało. Wystarczy, że oznaczymy metody adnotacją @Test. To jednak jeszcze nie jest wszystko. Musimy skonfigurować całą klasę tak, aby bean uzytkownikDao został wstrzyknięty do klasy testowej. W tym celu wskazujemy Springowi, skąd ma wziąć definicję bean'a oraz w którym miejscu ma on zostać wstrzyknięty. Aby tego dokonać umieszczamy przed atrybutem uzytkownikDao adnotację @ContextConfiguration z parametrem locations zawierającym listę plików xml z definicjami bean'ów, a także adnotację @Resource z wskazaniem nazwy bean'u, który ma zostać wstrzyknięty. Parametr name może zostać w zasadzie pominięty. Spring sam sobie poradzi przy założeniu, że wśród bean'ów mamy tylko jeden typu UzytkownikDao:

@ContextConfiguration(locations = {"classpath:beans.xml"})
public class UzytkownikDaoTest {

	@Resource(name = "uzytkownikDao")
	private UzytkownikDao uzytkownikDao;

	// Reszta klasy
}

Aby wskazać Springowi, że jest to klasa testowa, oznaczamy ją dodatkową adnotacją @RunWith ze wskazaniem klasy, która będzie uruchamiać testy. W przypadku JUnit4 będzie to SpringJUnit4ClassRunner:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:beans.xml"})
public class UzytkownikDaoTest {

	// Reszta klasy
}

Aby sprawić, że testy odbywać się będą w transakcjach (każdy w oddzielnej), musimy jeszcze dodać na początku adnotację @Transactional:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:beans.xml"})
@Transactiona
public class UzytkownikDaoTest {

	// Reszta klasy
}

Na koniec jeszcze kilka słów na temat konfiguracji kontenera bean'ów, która według wcześniejszych ustaleń powinna znajdować się w pliku beans.xml gdzieś w classpath:

beans.xml



  
  

  
    
  
 
  
    
    
    
      
        org.hibernate.dialect.MySQLDialect
        true
        false
      
    
    
      
        pl.jedenpies.blog.domain.Uzytkownik
      
    
  

  
    
    
    
    
  

  

Należy zwrócić uwagę na kilka istotnych elementów:

  1. Tag <context:annotation-config> jest wymagany, abyśmy mogli wstrzykiwać zależności z użyciem adnotacji @Resources zarówno w klasie testowej jak i implementacji interfejsu UzytkownikDao.
  2. Tag <tx:annotation-driven> jest wymagany, abyśmy mogli używać adnotacji @Transactional w celu określenia, że metody klasy są objęte transakcją. Atrybut transaction-manager wskazuje na nazwę bean'a będącego menadżerem transakcji, na przykład takim jak ten w powyższym pliku.
  3. Pozostałe rzeczy, takie jak bean'y transactionManager, sessionFactory, dataSource zależą od przyjętych przez nas rozwiązań.

Jeśli mamy projekt poprawnie skonfigurowany pod Mavena, wystarczy uruchomić mvn test i cieszyć się wykonującymi się transakcyjnie testami jednostkowymi.

Problemy jakie napotkałem podczas implementacji testów:

  • Problem z zależnościami Spring'a opisany tutaj.
  • Przez pewien czas nie mogłem dojść dlaczego, mimo iż logi pokazują, że transakcja po teście została cofnięta (rollback), modyfikacje danych pozostawały w bazie. Okazało się, że winę ponosi silnik MySQL, który domyślnie tworzy tabele typu MyISAM nie obsługującego transakcji. Aby sobie z tym poradzić należy dodać ENGINE=InnoDB na końcu polecenia tworzącego tabelę.

Komentarze

Łukasz, 14 października 2011 13:54:25

W temacie masz: Testy jednostkowe w Spring 3.0, a w beans.xml używasz 2.0 - 2.5, taka mała uwaga :-)
Ogólnie artykuł ciekawy i dużo pomógł.
Pozdro.

Zostaw komentarz

Treść:
Podpis:
Strona WWW:
Kod:

Weryfikacja antyspamowa