Hibernate için Testlerin Yazılması

Hibernate için Unit testlerin yazılması, başlı başına bir sorun. Gerçi Hibernate kullanmasanızda, başka kütüphanelerde kullansanız, veritabanından verilerinizi doğrudan da çekseniz bu sorun olacaktır. Normal şartlarda bir proje geliştirilirken en azından üç farklı veritabanınız olmalıdır. Development veritabanınız genelde yereldir ve üzerinde çok önemli bilgiler barındırmaz. Çoğunlukla Development veritabanınız size sorun çıkarmaz. Adeta geliştirmekte olduğunuz programınızın sorunsuz çalışması için yaratılmıştır. Production veritabanınız ise geliştirme sonunda, gerçek dünyada kullandığınız veritabanınızdır. Bu kısımdan çok bahsetmeye gerek yok sanımıyorum. Eğer iki veritabanı kullanan türdenseniz, Production veritabanınız uygulamanızı derinden sarsacaktır. İşte bu derinden sarsmaları yaşamak istemiyorsanız Test veritabanınızı oluşturmalısınız.

Ne yazık ki herşey Test veritabanınızı oluşturmakla bitmiyor. Test veritabanınızı oluşturmak işin en kolay kısmı. Zor olan kısm, test veritabanınızda yazdığınız testlerin gerçeklenmesidir. Fakat bir şeyi test etmek için, test edeceğiniz bilginin şu anki konumunu bilmelisiniz. Sonuçta hibernate kısmında yazdığınız tüm eklemeler, çıkarmalar ve güncellemeler sadece veritabanınızın durumunu değiştirmektedir. Eğer veritabanınızın başlangıç durumunu bilmiyorsanız, yaptığınız değişikliklerin sonucunu tam olarak bilemezsiniz. Bundan dolayı testleriniz çok gereksiz bir hal alır. Doğru çalışacağını bildiğiniz testler yazarsınız ve bu testler her zaman çalışır. Tabi en iyi ihtimalle her zaman çalışır. Projeniz ilerlediğinde hiç çalışmaz hale de gelmeleri mümkündür. Özetle bu tip testler sizi bir yere götürmez.

Ayrıca bir tablonun verileriyle oluşturulmuş bir yapıyı test ediyorsanız, tablonun hem ortalama doluluktaki durumuna göre, hem boş olduğu duruma göre ve hemde aşırı yüklü olduğu durumlara göre testler yapmanız gerekir. Basit bir MySQL tablosunu bu şekilde test etmeniz zaman açısından pek hoş olmayacaktır. Sonuçta yazdığınız her testin, her build zamanında teker teker çalışması gerekmektedir. Bu durumda zaman sizin için çok önemli bir hal alır.

Hibernate, DBunit ve HsqlDb

Tüm bu sorunları çözmek mümkün. Veritabanınızın şu anki durumunu bilmek için HsqlDb ve DBunit kullanabilirsiniz. HsqlDb tamamen Java ile yazılmış bir veritabanı. Güzel özelliği, size anlık olarak bellek üzerinde çalışan bir veritabanı sunucusu yaratabiliyor. Hibernate ile çok uyumlu çalışan bu veritabanı üzerinde, hibernate sizin için Entity sınıflarınıza ait veritabanlarını yaratıyor. Tabi yaratmada ki becerisi ile sizin Entity yazmanızdaki beceriniz bağlantılı. Örneğin benim yazdığım HibernateJPAExample(Git) projesinde, MySql üzerinde Detele işlemi Cascade ile düzgün bir şekilde çalışırken, HsqlDb üzerinde hata verebiliyor. Bu sebepten Entity’lerinizi yaratırken tüm nitelikleri düzgün verdiğinizden emin olun.

DBunit ise HsqlDb ya da başka bir test veritabanınızın durumunu ayarlamanıza yardımcı oluyor. Örneğin Dbunit ile, veri setleri tanımlayabiliyorsunuz. Dbunit ise bu setleri sizin için veritabanınıza kayıt ediyor. İsterseniz bunu önce veritabanınızı temizleyip öyle yapıyor. Aynı şekilde Dbunit’te hibernate ile uyumlu çalışacak şekilde ayarlanabiliyor. Dbunit sınıfınıda istediğiniz gibi ayarladıktan sonra tek yapmanız gereken Junit test sınıflarınızı yazmak kalıyor.

Maven

Daha fazla ilerlemeden önce, maven bağımlılıklarımızı girelim. Aşağıdaki satırları pom.xml dosyanıza eklemeniz yeterli olacaktır. Tabi buradaki bağımlılıklar sadece test kısmı için yoksa hibernate ve loglama içinde bağımlılıklarınız olacaktır. Ayrıca belirtmemde fayda var, internetteki tüm Dbunit örnekleri Spring üzerinden anlatılmış. Dikkat ettiyseniz testlerimiz için hiç bir Spring bağımlılığına ihtiyacınız yok. Zaten sadece test için Spring kullanmaya gerekte yok. Fakat projenizde Spring kullanıyorsanız testler için de kullanmanızı öneririm.

pom.xml

	<dependencies>
...
                <!-- DEPENDENCIES FOR TESTING -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>4.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.hsqldb</groupId>
			<artifactId>hsqldb</artifactId>
			<version>2.2.8</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.dbunit</groupId>
			<artifactId>dbunit</artifactId>
			<version>2.4.8</version>
			<scope>test</scope>
		</dependency>
...
	</dependencies>

Maven bağımlılıklarımızı eklediğimize göre projemize başlayabiliriz. Tüm DAO ve Entity sınıflarını sıfırdan yazarak uğraşmayacağım. Bu sınıfları ve tüm projeyi Git Depomdan indirebilirsiniz. Yinede kısaca projeden bahsetmekte fayda var. Proje içerisinde iki adet entity sınıfım mevcut bunlar Customer ve CustomerOrder tahmin edebileceğiniz üzere CustomerOrder ile Customer tabloları arasında N-1 ilişki var. Aynı şekilde bunlar için tüm DAO(Data Access Object)  sınıfları yazılmış durumda. Tabi bunlar için Development ortamında kullanılacak olan hibernate.cfg.xml dosyasıda src/main/resources klasörü altında bulunuyor. Buraya kadar zaten herşey normal bir projede olması gerektiği gibi. İşler bundan sonra değişmeye başlıyor.

HsqlDb

Dbunit ve veri setlerine geçmeden önce ilk iş olarak Hsqldb ayarlarının yapılması gerekiyor. Daha doğrusu Hsqldb üzerinde çalışacak şekilde bir hibernate.cfg.xml dosyasının yaratılması gerekiyor. Tabi bu dosya Development ortamı için kullandığımız xml dosyasından farklı. En başta bahsettiğimiz gibi Test ve Development veritabanlarını ayırmamız gerektiğinden ayar dosyalarınında ayrı olması gerekmektedir.

hibernate.cfg.xml dosyamızı src/test/resources klasörümüze çıkartıyoruz. Bildiğiniz üzere src/test/resources maven tarafından tanımlı olan kaynak kodu klasörlerinden biri. Bu sebepten eğer projeniz içerisinde gözükmüyorsa çekinmeden ekleyebilirsiniz. Fakat projenizin tipine göre bu klaösürn içeriğinin output klasörüne taşınmasını istemeyebilirsiniz. Zaten taşınırsa main altında oluşturulmuş hibernate.cfg.xml dosyanızda ezilecektir. Ondan dolayı Build Path kısmından src/test/resources klasörünün tümü exclude edin.

HsqlDb server’ımımız bellek üzerinde çalışacak şekilde ayarlamak için aşağıdaki hibernate.cfg.xml dosyasını örnek alabilirsiniz.

<!--?xml version="1.0" encoding="UTF-8"?-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
                                         "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">
<hibernate-configuration>
	<session-factory>
		<property name="hibernate.connection.driver_class">org.hsqldb.jdbcDriver</property>
		<property name="connection.url">jdbc:hsqldb:mem:hibernatejpa-test</property>
		<property name="hibernate.connection.username">sa</property>
		<property name="hibernate.connection.password"></property>
		<property name="hibernate.dialect">org.hibernate.dialect.HSQLDialect</property>
		<property name="hibernate.current_session_context_class">org.hibernate.context.ThreadLocalSessionContext</property>
		<!--
		<property name="hibernate.current_session_context_class">org.hibernate.context.JTASessionContext</property>
		<property name="jta.UserTransaction">java:comp/UserTransaction</property>
		-->
		<!--  <property name="hibernate.current_session_context_class">org.hibernate.context.ThreadLocalSessionContext</property> -->
		<!-- <property name="hibernate.show_sql" >false</property>   <property name="hibernate.format_sql">true</property>  -->

		<!-- JDBC connection pool (use the built-in) -->
                <property name="connection.pool_size">1</property>
		<property name="hbm2ddl.auto">update</property>

		<mapping class="com.bahadirakin.persistance.model.Customer" />
		<mapping class="com.bahadirakin.persistance.model.CustomerOrder"/>
	</session-factory>
</hibernate-configuration>

Burada öncelikle dikkat etmeniz gereken kısım veritabanı bağlantı adresinin bellek üzerinde verildiğidir. Hsqldb normal veritabanı bağlantılarını desteklese de testlerimiz için bellek bağlantısı daha kolay olmaktadır. Aynı şekilde gerekli öznitelikler tabloların oluşturulması üzerine kurulmuştur. Bu sayede Entity objelerimiz tablolara dönüştürülecektir. Böylece Dbunit kendi veri setlerini rahatlıkla yükleyebilecektir. Eğer başka bir veritabanı kullanmak isteniyorsa, tek yapılması gereken hibernate.cfg.xml dosyanızı kullanacağınız veritabanına göre ayarlanması

Dbunit

Sırada Entity nesneleriniz için veri setlerin yazılması kalıyor. Burada Dbunit size farklı farklı alternatifler sunabiliyor. Dbunit için farklı tipte ver set yapıları mümkün. Fakat yeni başlayan birin için en kolayı FlatXmlDataSet olacaktır. Burada xml dosyaları içinde tablo satırları yer almaktadır. Her xml dosyasında farklı farklı tablolar için istediğiniz kadar satır girebilirsiniz. Tabi bunları DTD(Document Type Definition) dosyanız içerisinde kısıtlamanızda mümkün. DTD dosyası çoğu oturmuş xml dosyasında olan ve o xml dosyasında nelerin olup nelerin olamayacağını bildiren dosyadır. Dikkat ederseniz bu tip dosyaları hibernate.cfg.xml gibi ayar dosyalarında da fark edeceksinizdir.

Özetle Dbunit ile kullanılabilecek veri setlerinizi oluşturmak için iki farklı dosyaya ihtiyacaınız olacak. Bunlardan biri Dataset.xml dosyası diğeri ise my-dataset.dtd dosyasıdır. İsimleri tamamen benim tarafından verilmiştir. Değiştirmek isterseniz değiştirebilirsiniz. Fakat kullanıldıkları yerlere dikkat etmenizi öneririm aksi halde hataya yol açabilir. Yine bu iki dosyayıda src/test/resources klasörümüze yaratıyoruz.

Dataset.xml

<!DOCTYPE dataset SYSTEM "my-dataset.dtd">
<dataset>
    <CUSTOMER ID="123" NAME="Bahadır AKIN" ADDRESS="Ortaköy" CITY="İstanbul" PHONE="555-5555555" />
    <CUSTOMERORDER ID="321" CUSTOMERID="123" DATEPLACED="2012-12-31" DATEPROMISED="2012-12-12" STATUS="Statusm"/>
</dataset>

my-dataset.dtd

<!ELEMENT dataset (
    CUSTOMER*,
    CUSTOMERORDER*)>

<!ELEMENT CUSTOMER EMPTY>
<!ATTLIST CUSTOMER
    ID CDATA #REQUIRED
    ADDRESS CDATA #IMPLIED
    CITY CDATA #IMPLIED
    NAME CDATA #IMPLIED
    PHONE CDATA #IMPLIED
>

<!ELEMENT CUSTOMERORDER EMPTY>
<!ATTLIST CUSTOMERORDER
    ID CDATA #REQUIRED
    DATEPLACED CDATA #IMPLIED
    DATEPROMISED CDATA #IMPLIED
    STATUS CDATA #IMPLIED
    CUSTOMERID CDATA #IMPLIED
>

Dataset ve dtd dosyaları için öncelikle şunu söylemeliyim. Normal şartlarda büyük harf ya da küçük harf yazmanız hiç birşeyi değiştirmiyor. Fakat Türkçe düşünüp İngilizce yazdığımızdan sorun yaşıyoruz. Demek istediğim “id” yazacak olursanız bu “İD” olarak algılanabiliyor. Böylelikle sizde böyle bir sütun bilgisi yok ya da böyle bir tablo bilgisi yok gibisinden hata alabiliyorsunuz. Ondan dolayı siz siz olun her zaman büyük harf kullanın.

Daha önce hiç dtd dosyası yazmamış biri olarak, ilk yazarken oldukça zorlandım. Dbunit sizi düşünmüş ve verilen bağlantı üzerinden sizin için dtd dosyasını oluşturuyor. Bunun nasıl yapıldığını birazdan anlatacağım BaseDaoTestHibernateImpl sınıfında bulabilirsiniz.

Unit Testlerin Yazılması

Sırada testlerimizi asıl yapacak sınıfların yazılması var. Fakat ondan önce soyut sınıfımızı yapıp biraz işimizi kolaylaştıralım. Bu sınıf ile;

  • Dbunit veri setinden bilgiler çekilerek tablolarımızın içeriği doldurulacak.
  • Hibernate kısmından connection bilgisi çekilecek.
  • Dbunit tarafından kullanılan bağlantı yapısına çevrilecek.
  • Bundan sonra yazılacak her veritabanı test sınıfı bu soyut sınıftan türeyecek.

BaseDAOTestHibernateImpl.java

package com.bahadirakin.persistance;

import java.io.FileOutputStream;

import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatDtdDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.dbunit.util.fileloader.FlatXmlDataFileLoader;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.junit.After;
import org.junit.Before;

/**
 * {@link IBaseDAOTest} implemented for using with Hibernate and HSQLDB
 *
 *
 * @author Bahadır AKIN
 *
 */
public abstract class BaseDAOTestHibernateImpl implements IBaseDAOTest {

	/**
	 * Hibernate {@link Session} for DBUNIT insert and clean operations
	 */
	private Session session;
	/**
	 * Hibernate {@link Transaction} for DBUNIT insert and clean operations
	 */
	private Transaction transaction;
	/**
	 * The Test is initialized or not
	 */
	private boolean initialized = false;

	@Before
	public void setUp() throws Exception {
		initialize();
	}

	@After
	public void tearDown() {
		destroy();
	}

	public void initialize() throws Exception {
		session = getSessionFactory().openSession();
		transaction = session.beginTransaction();
		initialized = true;
		DatabaseOperation.CLEAN_INSERT.execute(getConnection(), getDataSet());
		transaction.commit();
		transaction.begin();
	}

	public void destroy() {
		transaction.commit();
		session.close();
		initialized = false;
	}

	@SuppressWarnings("deprecation")
	public IDatabaseConnection getConnection() throws Exception {
		if (!initialized)
			throw new Exception("Initialize method must be called during SetUp(Before)");
		return new DatabaseConnection(session.connection());
	}

	public IDataSet getDataSet() throws Exception {
		return new FlatXmlDataFileLoader().getBuilder().build(getDataSetFile());
	}

	public void extractDTD(String outputFilePath) throws Exception {
		FlatDtdDataSet.write(getConnection().createDataSet(),
				new FileOutputStream(outputFilePath));
	}

	/**
	 * Providing Hibernate {@link SessionFactory} may be differ from test to
	 * test.
	 *
	 * @return
	 */
	public abstract SessionFactory getSessionFactory();

}

Şimdi ise bu sınıfımızı kullanarak CustomerDAO sınıfımızın test sınıfını yazalım.

CustomerDAOImplTest .java

package com.bahadirakin.persistance.dao;

import java.io.File;

import junit.framework.Assert;

import org.hibernate.SessionFactory;
import org.junit.Test;

import com.bahadirakin.persistance.BaseDAOTestHibernateImpl;
import com.bahadirakin.persistance.HibernateUtil;
import com.bahadirakin.persistance.dao.impl.CustomerDAOImpl;
import com.bahadirakin.persistance.dao.impl.CustomerOrderDAOImpl;
import com.bahadirakin.persistance.model.Customer;
import com.bahadirakin.persistance.model.CustomerOrder;

public class CustomerDAOImplTest extends BaseDAOTestHibernateImpl{

	public File getDataSetFile() {
		return new File("src/test/resources/Dataset.xml");
	}

	@Override
	public SessionFactory getSessionFactory() {
		return HibernateUtil.getInstance().getFactory();
	}

	@Test
	public void assertDB() {
		ICustomerDAO customerDAO = new CustomerDAOImpl();
		Customer customer = customerDAO.getById(123, false);
        ICustomerOrderDAO customerOrderDAO = new CustomerOrderDAOImpl();
        CustomerOrder customerOrder = customerOrderDAO.getById(321, false);

        customerOrderDAO.delete(customerOrder);
        customerDAO.delete(customer);
        Assert.assertTrue(customerDAO.getAll(true).size() == 0);
        Assert.assertTrue(customerOrderDAO.getAll(true).size() == 0);
	}
}

Junit ile test sınıfımızı çalıştırdığımızda bir sorun ile karşılaşmayacaksınız. Aynı şekilde maven ile projenizi derlerken bu test sınıflarınız her seferinde çalışacak böylelikle bir taşla iki kuş vurmuş olacaksınız.

Son

Umarım test kısmının nasıl yapılması gerektiği ile ilgili azda olsa bir fikriniz oluşmuştur. Zaten bir yerden sonrası kendi projeniz ile şekillenecektir. Yinede tüm projeye ihtiyacaınız olursa aşağıdaki linkten indirebilirsiniz.

Git Repository: Hibernate-Examples

End Of Line