Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database


Pada tulisan Menguji Halaman Web Secara Otomatis, saya menuliskan bagaimana cara memakai Selenium untuk menguji web front end secara otomatis.   Permasalahan yang timbul dengan memakai cara tersebut adalah selama pengujian, saya harus menjalankan aplikasi di server development secara manual dan memakai database yang sama dengan yang dipakai selama development.  Hal ini akan menyebabkan masalah konsistensi data jika seandainya pengujian gagal di tahap tertentu.  Selain itu, bila saya menguji halaman seperti Pemesanan, saya harus menambah Stok baru, menambah User baru, kemudian membuka halaman Pemesanan, lalu menghapus Stok, dan menghapus User.

Langkah yang ditempuh sangat melelahkan sekali, membuat saya menetaskan keringat dingin saat menulis kode program untuk pengujian web front end.  Entah bagaimana para extreme programmer dengan Test Driven Development (TDD)-nya bisa bertahan hidup.  Akhirnya muncul sebuah ide untuk memakai database in-memory yang terpisah dari database development.   Selain itu, saya ingin mengisi database tersebut dengan data secara otomatis dan konsisten setiap kali sebuah method di test class dikerjakan.  Dengan demikian, saya bisa langsung berkonsentrasi pada halaman yang akan diuji, tanpa perlu menambah item-item yang diperlukan di halaman lain.   Saya tidak tahu apakah ini masih masuk dalam kategori functional testing atau unit testing, tapi cara ini akan mempermudah hidup saya (lagipula saya bukan penganut extreme programming!).

Ok, teknologi apa yang saya butuhkan?

  1. Jetty untuk menjalankan aplikasi web secara otomatis selama pengujian front-end.
  2. H2 Database sebagai database in-memory embedded untuk pengujian.
  3. DbUnit untuk mengisi database H2 berdasarkan data dari file Microsoft Excel yang disediakan.  Data  di-isi secara otomatis  setiap kali method yang dikerjakan (dengan kata lain, isi database akan kembali sama seperti isi file Microsoft Excel setiap kali sebuah method dikerjakan).
  4. Selenium, tentunya, untuk melakukan pengujian di browser.

Untuk men-download semua artifact JAR yang dibutuhkan secara otomatis, saya akan menambahkan dependency berikut ini pada Maven:

  1. org.eclipse.jetty.aggregate : jetty-all : 7.6.7.v20120910 [test]
  2. org.apache.tomcat : juli : 6.0. 36 [test] (karena saya memakai implementasi JSP dari Tomcat)
  3. com.h2database : h2 : 1.3.167
  4. org.dbunit : dbunit : 2.4.8 [test]
  5. org.apache.poi : poi : 3-2-FINAL [test]
  6. org.seleniumhq.selenium : selenium-java : 2.25.0 [test]
  7. org.seleniumhq.selenium : selenium-server : 2.25.0 [test]

Struktur proyek yang saya pakai adalah struktur proyek Maven yang terlihat seperti pada gambar berikut ini:

Struktur Proyek

Struktur Proyek

Saya akan meletakkan kode program proyek di src/main/java dan file konfigurasi yang dibutuhkan di src/main/resources.  Khusus untuk kode program yang mewakili test case pengujian, saya meletakkannya di src/test/java.  File-file yang dibutuhkan untuk pengujian akan saya letakkan di src/test/resources.   Dengan struktur proyek seperti ini, pada saat aplikasi akan di-deploy atau di-built, hanya folder src/main saja yang akan disertakan (karena src/test untuk pengujian bukan untuk dipakai user).

Berikutnya saya akan membuat sebuah class bernama AbstractSeleniumTest.  Class ini nantinya wajib di-extends oleh semua class yang akan melakukan pengujian front end.  Isi dari class AbstractSeleniumTest adalah:

import static org.junit.Assert.fail;

import java.util.ArrayList;
import java.util.List;

import org.dbunit.DataSourceDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.util.fileloader.XlsDataFileLoader;
import org.eclipse.jetty.plus.jndi.EnvEntry;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.webapp.WebAppContext;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverBackedSelenium;
import org.openqa.selenium.firefox.FirefoxDriver;

import com.thoughtworks.selenium.Selenium;

public class AbstractSeleniumTest {

	protected static Server server;
	protected static DataSourceDatabaseTester dataTester;
	protected static JdbcDataSource dataSource;

	protected Selenium selenium;	

	@BeforeClass
	public static void beforeTestClass() throws Exception {

		// Setup Database H2 In-Memory
		if (dataSource==null) {
			dataSource = new JdbcDataSource();
			dataSource.setURL("jdbc:h2:mem:test;DB_CLOSE_DELAY=-1");
			dataTester = new DataSourceDatabaseTester(dataSource);
		}

		// Setup Server Jetty
		if (server==null || !server.isRunning()) {
			Server server = new Server(8181);
			server.setAttribute("org.eclipse.jetty.webapp.Configuration",
				new String[]{"org.eclipse.jetty.plus.webapp.EnvConfiguration"});
			EnvEntry envEntry = new EnvEntry("java:comp/env/jdbc/testdatabase", dataSource);
			server.addBean(envEntry);

			WebAppContext context = new WebAppContext();
			context.setDescriptor("../FolderProyek/src/main/webapp/WEB-INF/web.xml");
			context.setResourceBase("../FolderProyek/src/main/webapp");
			context.setContextPath("/proyek");
			context.setParentLoaderPriority(true);

			List listOverrideDescriptors = new ArrayList();
			listOverrideDescriptors.add("override-web.xml");
			context.setOverrideDescriptors(listOverrideDescriptors);

			server.setHandler(context);			
			server.start();
		}
	}

	@AfterClass
	public static void afterTestClass() throws Exception {
	}

	@Before
	public void setUp() {

		// Database Setup
		XlsDataFileLoader xlsDataFileLoader = new XlsDataFileLoader();
		DataSets dataSetsAnnotation = this.getClass().getAnnotation(DataSets.class);
		if (dataSetsAnnotation!=null) {
			IDataSet dataSet = xlsDataFileLoader.load(dataSetsAnnotation.setUpDataSet());
			dataTester.setDataSet(dataSet);
			try {			
				dataTester.onSetup();
			} catch (Exception ex) {
				fail("Terjadi kesalahan [" + ex.getMessage() + "]");
			}
		}

		// Selenium Setup
		WebDriver driver = new FirefoxDriver();
		String baseUrl = "http://localhost:8181/";
		selenium = new WebDriverBackedSelenium(driver, baseUrl);
	}

	@After
	public void tearDown() {
		if (dataTester!=null) {
			try {
				dataTester.onTearDown();
			} catch (Exception ex) {
				fail("Terjadi kesalahan [" + ex.getMessage() + "]");
			}
		}
		if (selenium!=null) {
			selenium.stop();
		}
	}
}

Kode program pada method beforeTestClass() yang diberi annotation @BeforeClass akan dikerjakan oleh JUnit setelah instance class untuk pengujian dibuat.  Di method ini, saya membuat sebuah database in-memory H2, lalu membuat embedded  Jetty dan melewatkan data source H2 melalui JNDI sehingga bisa dipergunakan oleh aplikasi yang diuji yang berjalan di dalam Jetty.   Pada method ini saya melakukan penjagaan null untuk memastikan bahwa database dan server Jetty hanya dibuat 1 kali saja.

Method afterTestClass() yang diberi annotation @AfterClass akan dikerjakan setelah seluruh test case pada object tersebut selesai dikerjakan.  Saya tidak melakukan apa-apa disini.  Saya tidak menutup database dan server Jetty, karena mereka masih diperlukan untuk class berikutnya.  Saya tidak tahu class mana yang dikerjakan terakhir kali.  Lalu kapan database dan server Jetty akan ditutup?  Pada saat pengujian berakhir dimana  JVM JUnit dimatikan, segala sesuatu yang dibuat oleh JVM  termasuk database dan server akan dibuang dari memory secara otomatis.

Saya masih perlu membuat sebuah class lagi, yaitu annotation DataSets.  Saya melihat trik memakai DataSets ini di buku JUnit in Action (2nd edition) dan buku Spring 3 Pro.  DataSets adalah sebuah annotation yang berisi informasi file Excel apa yang akan di-load ke database oleh DbUnit.  Setiap sheet di Excel mewakili sebuah tabel, setiap kolom dalam sheet mewakili field di tabel, dan setiap baris dalam sheet mewakili record di tabel.  Pada kedua buku tersebut, DataSets akan diperiksa untuk setiap method.  Akan tetapi karena saya tidak menggunakan SpringJUnit4ClassRunner seperti di buku, saya tidak bisa memeriksa per-method.  Jalan terbaik yang bisa saya lakukan adalah memeriksa per-class.  Berikut ini adalah isi annotation DataSets saya:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataSets {

	String setUpDataSet() default "";

}

Sekarang, aplikasi saya bisa berjalan pada 2 container, yaitu secara normal di tc Server (Tomcat 6) dan selama pengujian selenium di Jetty.  Jika aplikasi saya berjalan di tc Server, ia akan mengakses database MySQL.  Jika aplikasi saya berjalan di Jetty, maka ia akan mengakses database in-memory H2.  Hal ini dapat diatur dengan mudah berkat Spring profiles.  Berikut ini adalah konfigurasi Spring saya:

...
<bean id="parentJpaProperties" abstract="true" class="org.springframework.beans.factory.config.PropertiesFactoryBean">
 <property name="properties">
   <props>
     <prop key="hibernate.max_fetch_depth">3</prop>
     <prop key="hibernate.jdbc.fetch_size">50</prop>
     <prop key="hibernate.jdbc.batch_size">10</prop>
     <prop key="hibernate.id.new_generator_mappings">true</prop>
     <prop key="hibernate.ejb.naming_strategy">org.hibernate.cfg.ImprovedNamingStrategy</prop>
   </props>
 </property>
</bean>

<beans profile="dev,production">
 <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" >
   <property name="driverClassName" value="com.mysql.jdbc.Driver" />
   <property name="url" value="jdbc:mysql://localhost:3306/database" />
   <property name="username" value="user" />
   <property name="password" value="password" />
 </bean>

 <bean id="jpaProperties" parent="parentJpaProperties">
   <property name="properties">
     <props merge="true">
       <prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
     </props>
   </property>
 </bean>
</beans>

<beans profile="selenium-test">
 <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/testdatabase" />

 <bean id="jpaProperties" parent="parentJpaProperties">
   <property name="properties">
     <props merge="true">
       <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
       <prop key="hibernate.hbm2ddl.auto">create-drop</prop> 
     </props>
   </property>
 </bean>
</beans>
...

Informasi mengenai deklarasi inheritance yang saya pakai dapat dilihat di artikel Memakai Ulang Props Di Definisi XML Spring.  Yang jelas, pada saat aplikasi dijalankan dalam container Jetty, Spring profile yang aktif haruslah selenium-test.

Pertanyaan bagaimana membuat Spring profile selenium-test menjadi aktif saat aplikasi dijalankan dalam container Jetty?  Informasi lebih lanjut dapat dilihat di artikel Mengatur Spring Profile Untuk Embedded Jetty.   Berikut ini adalah isi file override-web.xml saya adalah:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">

  <context-param>
    <param-name>spring.profiles.active</param-name>
    <param-value>selenium-test</param-value>
  </context-param>

  <resource-ref>
    <res-ref-name>jdbc/testdatabase</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
  </resource-ref>

</web-app>

Setelah ini, saya bisa membuat file Excel yang berisi data yang akan dimasukkan ke database secara otomatis setiap kali sebuah method di test case dikerjakan.  File tersebut juga diletakkan di src/test/resources.  Pada akhirnya, isi folder tersebut akan terlihat seperti (file log4j.xml dan StressTest.jmx tidak termasuk dalam pembahasan di tulisan ini):

Isi folder src/test/resources

Isi folder src/test/resources

Terakhir, saya tinggal membuat class  yang memakai Selenium untuk menguji tampilan.  Berikut ini adalah contoh test case yang saya buat:

import static org.junit.Assert.*;
import org.dbunit.dataset.ITable;
import org.junit.Test;

@DataSets(setUpDataSet="/dataNormal.xls")
public class AkunSayaTest extends AbstractSeleniumTest {
  @Test
  public void testUpdateProfilUntukKaryawan() throws Exception {

  // Login Sebagai User PEGAWAI
  selenium.open("/proyek/");
  selenium.waitForPageToLoad("30000");
  selenium.click("link=Login");
  selenium.waitForPageToLoad("30000");
  selenium.type("id=login_user", "lena@gmail.com");
  selenium.type("id=login_pass", "lenalovejocki");
  selenium.click("name=login");
  selenium.waitForPageToLoad("30000");

  // Membuka Menu Akun Saya
  selenium.open("/proyek/#&panel1-2");
  selenium.click("link=Akun Saya");
  selenium.waitForPageToLoad("30000");

  // Men-klik menu Edit Profil
  selenium.click("id=editProfil");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==true", "1000");

  // Memastikan tampilan di dialog benar
  assertEquals("lena@gmail.com", selenium.getValue("id=email"));
  // .. dan sebagainya ...

  // Melakukan perubahan data		
  selenium.type("id=nama", "Jona Junior");
  selenium.click("id=frmUserbtnSimpan");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==false", "1000");

  // Memastikan data sudah tersimpan di database
  assertEquals("Jona Junior", dataTester.getConnection().
     createQueryTable("user", "select * from user where email='lena@gmail.com'").getValue(0, "nama"));

  // Kembali membuka menu Edit Profile
  selenium.click("id=editProfil");
  selenium.waitForCondition("JL.dialogUtama['dialogUser'].is(':visible')==true", "1000");

  // Memastikan tampilan di dialog benar
  assertEquals("lena@gmail.com", selenium.getValue("id=email"));
  // .. dan sebagainya ..
 }
}

Kode program pengujian front-end saya sekarang bisa lebih singkat dan lebih mudah dipahami.  Selain itu , kini saya  tidak perlu khawatir lagi untuk menambah/mengubah data sebelum dan setelah pengujian.

Perihal Solid Snake
I'm nothing...

6 Responses to Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database

  1. aeroyid mengatakan:

    Keren mas….

    Saya baru tahu kalau ada teknologi seperti ini..

    saya izin untuk di coba … !! dan saya RT di blog saya…!!

    salam Java Indonesia…

  2. aeroyid mengatakan:

    Oh ya,,,, saya mau nanya ,, dibagian mana yang scrip jetty itu di masukkan…!!!

    • Solid Snake mengatakan:

      Kode program yang menyiapkan dan menjalankan Jetty ada di class AbstractSeleniumTest di method beforeTestClass(). Karena setiap class pengujian, misalnya AkunSayaTest, diturunkan dari class AbstractSeleniumTest, maka setiap kali saya men-klik kanan class pengujian di Eclipse, memilih Run As, JUnit Test.. secara otomatis Jetty akan dijalankan.
      Kalo soal meletakkan dimana class AbstractSeleniumTest dan lainnya, boleh bebas, selama di folder src/test/java. Asalkan jangan lupa di-import (tekan Ctrl+Shift+O di Eclipse untuk import secara otomatis) bila akan memanggil class yang beda package.

  3. Ping-balik: Melakukan Front-End Testing Dengan Codeception | The Solid Snake

Apa komentar Anda?

Please log in using one of these methods to post your comment:

Logo WordPress.com

You are commenting using your WordPress.com account. Logout / Ubah )

Gambar Twitter

You are commenting using your Twitter account. Logout / Ubah )

Foto Facebook

You are commenting using your Facebook account. Logout / Ubah )

Foto Google+

You are commenting using your Google+ account. Logout / Ubah )

Connecting to %s

%d blogger menyukai ini: