Melakukan Pengujian Java Swing Dengan FEST

Pada aplikasi web, saya dapat melakukan pengujian view HTML dengan menggunakan Selenium.  Bagaimana dengan aplikasi desktop? Apakah saya juga bisa melakukan pengujian user interface?  Yup, bisa!  Salah satu tool yang dapat saya pergunakan adalah FEST (Fixtures for Easy Software Testing).

Untuk memakai FEST, saya perlu men-download fest-swing-1.2.zip dari http://code.google.com/p/fest. Di dalam file tersebut, saya akan menemukan file fest-swing-1.2.jar yang harus disertakan dalam proyek yang akan diuji. Selain itu, saya juga perlu menyertakan jar yang ada di dalam folder lib seperti fest-util-1.1.2.jar dan fest-assert-1.2.jar.  Btw, di saat sedang serius, saya lebih memilih memakai Maven untuk melakukan proses download secara otomatis dengan menyertakan dependency ke org.easytesting.fest-swing.

Saya akan membuat sebuah tampilan Swing yang akan diuji dengan menggunakan MigLayout seperti berikut ini:

package com.snake.view;

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPasswordField;
import javax.swing.JTextField;

import net.miginfocom.swing.MigLayout;

public class LoginView extends JFrame {

	private static final long serialVersionUID = -1977819063823609321L;

	private JTextField txtNama;
	private JPasswordField txtPassword;
	private JButton btnLogin;
	private JButton btnKeluar;

	public LoginView() {
		super("Login");

		setLayout(new MigLayout("", "[right][grow,fill]", "[][][nogrid]"));
		add(new JLabel("Nama Pengguna:"));
		add(txtNama = new JTextField(20), "wrap");		
		add(new JLabel("Password:"));
		add(txtPassword = new JPasswordField(20), "wrap");
		add(btnLogin = new JButton("Login"), "align center, gaptop 10");
		add(btnKeluar = new JButton("Keluar"));

		txtNama.setName("txtNama");
		txtPassword.setName("txtPassword");
		btnLogin.setName("btnLogin");
		btnKeluar.setName("btnKeluar");

		btnLogin.addActionListener(new ActionListener() {

			@Override
			public void actionPerformed(ActionEvent e) {
				if (txtNama.getText().equals("solid") &&
						String.valueOf(txtPassword.getPassword()).equals("snake")) {
					JOptionPane.showMessageDialog(LoginView.this, "Ok, login sukses!");
				} else {
					JOptionPane.showMessageDialog(LoginView.this, "Maaf, kamu bukan dia!");
				}
			}

		});

		setSize(500,500);
		pack();		
	}

	public static void main(String[] args) {
		LoginView loginView = new LoginView();
		loginView.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		loginView.setVisible(true);
	}

}

Lalu, untuk melakukan pengujian, saya membuat sebuah JUnit test seperti berikut ini:

package com.snake.view;

import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
import org.fest.swing.edt.GuiActionRunner;
import org.fest.swing.edt.GuiQuery;
import org.fest.swing.fixture.FrameFixture;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

public class LoginViewTest {

	private FrameFixture window;

	@BeforeClass
	public static void beforeClass() {
		FailOnThreadViolationRepaintManager.install();
	}

	@Before
	public void before() {

		LoginView loginView = GuiActionRunner.execute(new GuiQuery<LoginView>() {
			@Override
			protected LoginView executeInEDT() throws Throwable {
				return new LoginView();
			}
		});
		window = new FrameFixture(loginView);
		window.show();
	}

	@After
	public void after() {
		window.cleanUp();
	}

	@Test
	public void testTampilanAwal() {
		window.textBox("txtNama").requireEditable().requireEmpty();
		window.textBox("txtPassword").requireEditable().requireEmpty();
		window.button("btnLogin").requireVisible();
		window.button("btnKeluar").requireVisible();
	}

	@Test
	public void testLoginSukses() {
		window.textBox("txtNama").enterText("solid");
		window.textBox("txtPassword").enterText("snake");
		window.button("btnLogin").click();
		// Menunggu JOptionPane muncul dengan timeout default 100ms
		window.optionPane().requireMessage("Ok, login sukses!");
	}

	@Test
	public void testLoginGagal() {
		window.textBox("txtNama").enterText("user aneh");
		window.textBox("txtPassword").enterText("password ajaib");
		window.button("btnLogin").click();
		// Menunggu JOptionPane muncul dengan timeout default 100ms
		window.optionPane().requireMessage("Maaf, kamu bukan dia!");
	}
}

Pada pengujian di atas, saya memberikan statement FailOnThreadViolationRepaintManager.install() untuk mendeteksi apakah ada kode program yang mengakses Swing tetapi tidak berjalan di EDT. Saya membahas tentang EDT di tulisan Multithreading Dengan Mudah Di Griffon.   Pada tulisan tersebut saya memakai bahasa Griffon, sementara disini saya memakai bahasa Java.  Tapi keduanya sama-sama berjalan di platform Java sehingga prinsip dasar-nya tetap sama.    Bila terdapat pelanggaran saat JUnit dijalankan, pada console akan terdapat output seperti org.fest.swing.exception.EdtViolationException: EDT violation detected.

Ini adalah contoh hasil yang saya peroleh saat menjalankan JUnit test case di atas:

Hasil Pengujian

Hasil Pengujian

Pada saat memakai JUnit dengan Selenium, browser akan muncul dan men-klik “sana-sini” secara otomatis.  Begitu juga dengan sekarang.  Saat memakai FEST, JFrame yang diuji akan muncul, setiap JTextField akan di-isi, kemudian JButton akan di-click.

Menguji Halaman Web Secara Otomatis

Salah satu bagian yang cukup ribet dalam pengujian sebuah aplikasi adalah pengujian tampilan. Yup! Kita mungkin sudah menguji setiap class yang ada dengan JUnit, tapi bagaimana bila kesalahannya terletak di halaman web? Kita harus memastikan satu per satu, setiap halaman yang ada dapat berfungsi sebagaimana mestinya.  Kalau ada banyak halaman, selain repot juga membutuhkan ketelitian.  Oleh sebab itu, Selenium menyediakan sebuah cara otomatis untuk melakukan pengujian halaman web.

Sebagai contoh, saya memiliki sebuah halaman web dengan tampilan seperti berikut ini:

Halaman Web Yang Akan Diuji

Halaman Web Yang Akan Diuji

Bila pengguna mengisi nama user dan password dengan benar, maka halaman baru akan muncul. Tetapi bila pengguna mengisi nama user dan password dengan salah, maka akan muncul sebuah tulisan User atau password salah!.

Untuk melakukan pengujian secara otomatis, saya menggunakan IDE NetBeans 7.1.2, Firefox 13, dan Selenium yang baru di-checkout (repository URL SVN anonymous http://selenium.googlecode.com/svn/trunk/). Jangan lupa menambahkan dependencies secara manual ke NetBeans (termasuk file JAR yang dibutuhkan!). Bila memakai Maven, dapat juga langsung menambahkan dependency ke versi terbaru Selenium dengan Group Id: org.seleniumh.selenium, Artifact Id: selenium-java, dan Version: 2.24.1.

Cara yang paling gampang  untuk menguji halaman web adalah dengan memakai Selenium IDE terlebih dahulu. Selenium IDE adalah sebuah plugin untuk Firefox yang dapat di-download di http://seleniumhq.org/download/. Dengan plugin ini, saya dapat merekam aktifitas yang saya lakukan di sebuah halaman dan menerjemahkannya menjadi kode program. Sebagai contoh, saya akan merekam aktifitas mulai dari saat saya mengisi field Nama User, mengisi Password, hingga akhirnya menekan tombol Login. Lalu, sebagai penanda, saya men-select sebuah tulisan yang hanya akan muncul di halaman berikutnya bila login sukses. Saat men-klik kanan selection, saya akan memilih menu verifyTextPresent.

Untuk menghentikan perekaman, saya men-klik tombol lingkaran merah yang ada di kanan atas. Berikut ini contoh tampilan Selenium IDE yang berisi perintah yang mewakili aktifitas yang telah saya lakukan:

Tampilan Selenium IDE

Tampilan Selenium IDE

Berikutnya, masih di Selenium IDE, saya memilih menu File, Export Test Case As, lalu saya memilih JUnit 4 (WebDriver Backed). Saya akan meletakkan file baru ini di folder test/co/id/jocki/ui (saya memakai folder test bukan src).   Kemudian, saya memberikan nama LoginUITest sebelum men-klik tombol Save.

Berikut ini adalah  kode program yang dihasilkan beserta beberapa perubahan yang saya buat:

package co.id.jocki.ui;

import com.thoughtworks.selenium.SeleneseTestBase;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverBackedSelenium;
import org.openqa.selenium.firefox.FirefoxDriver;

public class LoginUITest extends SeleneseTestBase {

  @Before
  public void setUp() throws Exception {
    WebDriver driver = new FirefoxDriver();
    String baseUrl = "http://localhost:8080/";
    selenium = new WebDriverBackedSelenium(driver, baseUrl);
  }

  @Test
  public void testLoginSukses() throws Exception {
    selenium.open("/Test/login.jsp");
    selenium.type("id=namaUser", "user");
    selenium.type("id=password", "12345");
    selenium.click("//button[@type='button']");
    selenium.waitForPageToLoad("30000");
    verifyTrue(selenium.isTextPresent("Halaman Pengguna"));
  }

  @Test
  public void testLoginGagal() throws Exception {
    selenium.open("/Test/login.jsp");
    selenium.type("id=namaUser", "sembarangan");
    selenium.type("id=password", "sembarangan");
    selenium.click("//button[@type='button']");
    verifyTrue(selenium.isTextPresent("User atau password salah!"));
  }

  @After
  public void tearDown() throws Exception {
    selenium.stop();
  }
}

Pada kode program di atas, saya menambahkan dua buah pengujian (test case), yaitu pengujian login sukses dan pengujian login gagal. Pada pengujian login sukses, harus ada loading ke halaman baru, dimana pada halaman baru tersebut terdapat tulisan Halaman Pengguna. Pada pengujian login gagal, di halaman yang sama harus terdapat tulisan User atau password salah!. Bila perilaku yang terjadi berbeda seperti yang diharapkan, maka besar kemungkinan terdapat kesalahan di halaman web saya.

Untuk melakukan pengujian, saya men-klik kanan di editor NetBeans, kemudian saya memilih Test File (CTRL+F6).  Selama pengujian, Selenium akan membuka sebuah instance Firefox.  Selenium akan mengisi textbox yang ada dan men-klik tombol yang ada secara otomatis.  Saya tinggal menyaksikan hingga proses pengujian berakhir.  Btw, bila ingin menggunakan browser lain, FirefoxDriver di kode program di atas dapat diganti dengan class lain seperti InternetExplorerDriver atau ChromeDriver.  Juga ada driver untuk Opera, iPhone dan Android.

Di window Test Results, saya bisa melihat hasil pengujian, seperti yang terlihat di gambar berikut ini:

Window Test Results Di NetBeans

Window Test Results Di NetBeans

Kenapa harus repot-repot membuat skenario pengujian untuk halaman ini dan puluhan halaman lainnya? Memang repot untuk sekali ini, tapi bila pada suatu hari nanti saya melakukan modifikasi kode program (perubahan selalu ada!), saya tidak perlu lagi membuka  halaman yang ada satu per satu.  JUnit akan memberikan laporannya, dan saya tinggal mencari test case mana yang gagal (bila ada).  Sebuah kesalahan kecil kadang-kadang dapat berakar ke tempat tak terduga;  pengujian otomatis membantu mendeteksinya karena diterapkan pada keseluruhan kode program.