Belajar Membuat Service Di Android

Bila activity adalah bagian dari aplikasi yang berinteraksi dengan pengguna melalui UI, maka service adalah sesuatu yang berjalan di balik layar dalam jangka waktu lama. Service boleh dibiarkan tetap berjalan setelah aplikasi ditutup oleh pengguna. Untuk melihat daftar service yang sedang berjalan di sebuah perangkat Android, saya bisa memberikan perintah berikut ini:

$ adb shell dumpsys activity services

Sebagai latihan, saya akan membuat sebuah service yang menjadikan perangkat Android sebagai web server. Hal ini karena saya sering kali kesulitan bertukar file dari perangkat Android ke beberapa komputer dengan sistem operasi berbeda. Bila saya mempublikasikan file di media penyimpanan dalam bentuk web link, maka saya bisa secara mudah membaca file melalui browser di komputer.

Untuk itu, saya akan membuat sebuah proyek baru di Android Studio dengan nama MyWebServer. Saya memilih Blank Activity pada saat membuat proyek baru. Salah satu web server yang terkenal ringan dan sepenuhnya dibuat dengan menggunakan Java adalah Jetty. Saya dapat dengan mudah menyisipkan web server tersebut dalam aplikasi saya. Untuk menggunakan Jetty, saya menambahkan baris berikut ini pada file build.gradle (untuk module):

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'org.eclipse.jetty:jetty-server:9.2.7.v20150116'
}

Bila saya men-klik tombol Sync Now, Android Studio akan memerintahkan Gradle untuk men-download JAR Jetty yang dibutuhkan.

Setelah proses download selesai, saya siap untuk membuat sebuah service baru. Untuk itu, saya men-klik kanan nama package dan memilih menu New, Service, Service seperti pada gambar berikut ini:

Membuat service baru

Membuat service baru

Saya mengisi nama service dengan ServerService dan men-klik tombol Finish pada dialog yang muncul. Saya kemudian membuat kode program ServerService.java sehingga isinya menjadi seperti berikut ini:

package com.snake.mywebserver;

import ...

public class ServerService extends Service {

    private Server server;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (server == null) {
            String lokasiShare;
            if ((intent != null) && intent.hasExtra("lokasiShare")) {
                lokasiShare = intent.getStringExtra("lokasiShare");
            } else {
                lokasiShare = ".";
            }
            server = new Server(6666);
            ResourceHandler resourceHandler = new MyHandler();
            resourceHandler.setDirectoriesListed(true);
            resourceHandler.setResourceBase(lokasiShare);
            HandlerList handlers = new HandlerList();
            handlers.setHandlers(new Handler[]{resourceHandler, new DefaultHandler()});
            server.setHandler(handlers);
            try {
                server.start();
            } catch (Exception ex) {
                Log.e("ServerService", "Terjadi kesalahan", ex);
            }
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (server != null) {
            try {
                server.stop();
                server.destroy();
            } catch (Exception ex) {
                Log.e("ServerService", "Terjadi kesalahan", ex);
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

}

Pada kode program di atas, method onStartCommand() akan dikerjakan saat service dimulai dan method onDestroy() akan dikerjakan saat service dimatikan. Karena saya tidak pernah mematikan service secara manual dengan memanggil stopSelf(), maka service ini akan terus berjalan. Sistem operasi Android akan mematikan service bila ia membutuhkan memori ekstra. Karena saya mengembalikan START_STICKY pada onStartCommand(), setelah service dimatikan, Android akan berusaha menjalankannya kembali bila situasi sudah memungkinkan.

Pada saat mencoba menggunakan ResourceHandler bawaan Jetty, saya menemukan pesan kesalahan seperti berikut ini saat menampilkan direktori:

java.io.UnsupportedEncodingException: java.nio.charset.CharsetICU[UTF-8]

Untuk mengatasi hal tersebut, saya menghilangkan encoding=UTF-8 pada setContentType() dengan membuat sebuah turunan baru dari ResourceHandler yang saya beri nama MyHandler:

package com.snake.mywebserver;

import ...

public class MyHandler extends ResourceHandler {

    @Override
    protected void doDirectory(HttpServletRequest request, HttpServletResponse response, Resource resource) throws IOException {
        String listing = resource.getListHTML(request.getRequestURI(),request.getPathInfo().lastIndexOf("/") > 0);
        response.setContentType("text/html");
        response.getWriter().println(listing);
        response.setStatus(200);
    }

}

Karena Jetty akan membuka socket dan membaca file di media menyimpanan pada sistem operasi Android, saya perlu mendefinisikan penggunaan permission baru di AndroidManifest.xml:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Service telah selesai dibuat! Akan tetapi, pengguna tidak bisa langsung menjalankan service ini. Ingat bahwa service tidak mengandung interaksi UI. Untuk keperluan tersebut, saya bisa membuat sebuah activity baru dengan men-klik kanan nama package dan memilih menu New, Activity, Blank Activity. Pada kotak dialog yang muncul, saya tetap memakai nilai default dan memberi centang pada Launcher Activity sehingga activity ini dapat dijalankan dari menu utama. Saya kemudian mengubah isi layout activiy_main.xml menjadi seperti berikut ini:

<RelativeLayout ...>

    <Switch
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/status"
        android:textOn="Aktif"
        android:textOff="Mati"
        android:text="Status Web Server"
        android:layout_alignParentTop="true"
        android:layout_alignParentStart="true" />

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAppearance="?android:attr/textAppearanceMedium"
        android:text="Lokasi Share"
        android:id="@+id/textView"
        android:layout_below="@+id/status"
        android:layout_alignParentStart="true" />

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/lokasiShare"
        android:singleLine="true"
        android:layout_below="@+id/textView"
        android:layout_alignParentStart="true"
        android:layout_alignParentEnd="true" />

    <Button
        style="?android:attr/buttonStyleSmall"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Refresh Daftar Service"
        android:id="@+id/refresh"
        android:layout_below="@+id/lokasiShare"
        android:layout_alignParentStart="true"
        android:onClick="refreshDaftarService"
        android:nestedScrollingEnabled="true" />

    <ScrollView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:id="@+id/scrollView"
        android:layout_alignParentBottom="true"
        android:layout_alignEnd="@+id/lokasiShare"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/refresh">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:textAppearance="?android:attr/textAppearanceSmall"
            android:typeface="monospace"
            android:id="@+id/daftarService" />
    </ScrollView>
</RelativeLayout>

Tombol Refresh Daftar Service sebenarnya tidak berhubungan dengan servis yang saya buat. Saya menyertakannya supaya saya dapat menampilkan service yang sedang aktif di perangkat Android. Bila tombol tersebut disentuh, method refreshDaftarService() pada MainActivity akan dikerjakan:

public void refreshDaftarService(View view) {
    ActivityManager am = (ActivityManager) getSystemService(Activity.ACTIVITY_SERVICE);
    List<ActivityManager.RunningServiceInfo> services = am.getRunningServices(1000);
    StringBuilder hasil = new StringBuilder();
    for (ActivityManager.RunningServiceInfo info: services) {
        if (info.service.getPackageName().equals("com.snake.mywebserver")) {
            hasil.append("<font color='red'>");
        }
        hasil.append(info.service.flattenToShortString());
        hasil.append("; ");
        hasil.append(info.lastActivityTime);
        hasil.append("<br><br>");
        if (info.service.getPackageName().equals("com.snake.mywebserver")) {
            hasil.append("</font>");
        }
    }
    ((TextView) findViewById(R.id.daftarService)).setText(Html.fromHtml(hasil.toString()));
}

Kode program di atas memakai ActivityManager.getRunningServices() untuk mengembalikan seluruh service yang sedang aktif di perangkat Android. Saya juga menggunakan kode HTML untuk memberi warna merah pada saat menampilkan service com.snake.mywebserver. Berbeda dengan Swing dimana saya bisa langsung memasukkan HTML pada caption label atau tombol, pada Android, saya harus menggunakan Html.fromHtml().

Bila switch diaktifkan atau dimatikan, method prosesWebServer() pada MainActivity akan dikerjakan. Saya bisa membuat implementasi method tersebut seperti:

public void prosesWebServer(View view) {
    Switch sw = (Switch) view;
    Intent intent = new Intent(this, ServerService.class);
    EditText lokasiShare = (EditText) findViewById(R.id.lokasiShare);
    if (lokasiShare.getText().length() > 0) {
        intent.putExtra("lokasiShare", lokasiShare.getText().toString());
    }
    if (sw.isChecked()) {
        startService(intent);
    } else {
        stopService(intent);
    }
}

Agar switch dapat digeser (di-swipe), saya menambahkan event listener berikut ini pada saat onCreate():

findViewById(R.id.status).setOnTouchListener(new View.OnTouchListener() {
    @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (event.getActionMasked() == MotionEvent.ACTION_MOVE) {
            prosesWebServer(v);
            return true;
        }
        return false;
    }
});

Sekarang, saya dapat menggunakan activity ini untuk menjalankan service, seperti yang terlihat seperti pada gambar berikut ini:

Tampilan main activity

Tampilan main activity

Pada komputer PC dalam jaringan yang sama, saya dapat mengakses web dengan URL seperti http://192.168.1.2:6666 dimana 191.168.1.2 adalah IP lokal yang diberikan pada perangkat Android (melalui Wifi), seperti yang terlihat pada gambar berikut ini:

Mengakses file dari PC melalui browser

Mengakses file dari PC melalui browser

Perangkat Android sudah menjadi sebuah file server sederhana!

Sebagai latihan lebih lanjut, saya akan melakukan binding ke service yang sedang berjalan. Activity saat ini selalu menganggap bahwa service pada awalnya tidak aktif, terlihat pada status switch yang selalu Tidak aktif pada saat activity dijalankan. Akan tetapi, service bisa saja sudah aktif, misalnya pada urutan eksekusi seperti berikut ini:

  1. Jalankan activity.
  2. Jalankan service melalui activity. Service akan tetap berjalan walaupun activity ditutup.
  3. Tutup activity.
  4. Jalankan activity. Service yang dibuat masih berjalan tetapi switch untuk status berada di posisi tidak aktif.

Oleh sebab itu, akan lebih baik bila saya memeriksa apakah service sudah pernah dijalankan sebelumnya. Langkah pertama yang saya lakukan adalah menambah method baru pada ServerService:

public boolean isAktif() {
  return (server != null) && (server.isRunning());
}

Saya juga membuat sebuah implementasi dari IBinder di dalam ServerService , misalnya:

public class ServerService extends Service {

  private LocalBinder localBinder = new LocalBinder();
  ...

  public class LocalBinder extends Binder {

    ServerService getServerService() {
       return ServerService.this;
    }

 }

}

Dan terakhir, saya perlu mengubah kode program pada onBind() agar mengembalikan LocalBinder:

@Override
public IBinder onBind(Intent intent) {
  return localBinder;
}

Setelah itu, di sisi activity, saya perlu membuat sebuah implementasi ServiceConnection, misalnya:

package com.snake.mywebserver;

import ...

public class ServerServiceConnection implements ServiceConnection {

    private ServerService serverService;
    private Switch sw;

    public ServerServiceConnection(Switch sw) {
        this.sw = sw;
    }

    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        ServerService.LocalBinder binder = (ServerService.LocalBinder) service;
        this.serverService = binder.getServerService();
        updateSwitch();
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        this.serverService = null;
    }

    public void updateSwitch() {
        sw.post(new Runnable() {
            @Override
            public void run() {
                sw.setChecked(isAktif());
            }
        });
    }

    public boolean isAktif() {
        return (serverService != null) && (serverService.isAktif());
    }

}

ServiceConnection di atas akan men-update nilai sebuah switch tergantung pada status service saat ini.

Sebagai langkah terakhir, saya menambahkan kode program berikut ini pada MainActivity:

public class MainActivity extends ActionBarActivity {

    private ServerServiceConnection serverServiceConnection;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        bindService(serverIntent, serverServiceConnection, 0);
        sw.setOnTouchListener(...);
    }

    @Override
    protected void onStop() {
        super.onStop();
        if (serverServiceConnection != null) {
            unbindService(serverServiceConnection);
            serverServiceConnection = null;
        }
    }

    ...

}

Sekarang, bila saya menjalankan activity, status switch akan aktif atau tidak sesuai dengan kondisi apakah service sudah pernah dijalankan sebelumnya.

Iklan

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.

Mengatur Spring Profile Untuk Embedded Jetty

Pada saat menjalankan aplikasi di dalam embedded Jetty dengan tujuan pengujian, kadang-kadang saya perlu mengubah profile Spring yang sedang aktif, misalnya dari dev menjadi test.  Sebagai contoh, ini adalah potongan web.xml aplikasi yang saya buat:

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

Pada saat menjalankan embedded Jetty, saya memberikan referensi ke web.xml ini, misalnya seperti berikut:

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

Pertanyaannya adalah bagaimana mengubah nilai param spring.profiles.active dari dev menjadi test secara otomatis.  Yup, otomatis, karena tidak mungkin saya bisa dipaksa mengedit file web.xml secara manual setiap kali melakukan pengujian front-end (menjadi test) dan setiap kali menjalankan aplikasi secara normal (menjadi dev).

Sekilas terbersit bahwa Spring bisa membaca nilai profile yang aktif dari environment variable, sehingga harusnya saya bisa menambahkan System.setProperty(“spring.profiles.active”, “test”);  pada kode program di atas.  Sayangnya, hal seperti ini TIDAK bisa dilakukan, karena nilai pada web.xml yang dibaca akan MENIMPA nilai environment variable!!!

Lalu apa solusinya?  Saya harus memberitahukan pada Jetty bahwa nilai pada web.xml harus ditimpa oleh sesuatu nilai lain, misalnya dengan kode program berikut ini:

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

Pada kode program tersebut, saya harus menyediakan sebuah file override-web.xml yang berada dalam format yang sama seperti web.xml.  Bedanya, override-web.xml ini tidak perlu berisi seluruh definisi web.xml secara lengkap, melainkan hanya perlu menyertakan bagian yang akan diubah.  Sebagai contoh, untuk mengubah profile di Spring, isi 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>test</param-value>
  </context-param>

</web-app>

Memakai Jetty sebagai embedded web server

Jetty adalah sebuah web server yang unik.  Hal ini karena Jetty dapat dipakai sebagai embedded web server.  Apa maksudnya?  Biasanya pada aplikasi web, program yang telah kita buat perlu di-‘copy‘ ke sebuah container (web server).  Tapi embedded web server adalah kebalikannya.   Aplikasi kita didalamnya telah mengandung web server, sehingga kita tidak perlu memindahkan kode program ke web server lagi.  Lalu apa gunanya? Embedded web server dapat dipakai pada pengujian aplikasi dimana aplikasi akan dijalankan  pada embedded web  server yang tidak membutuhkan banyak setup serta tidak ‘berat.

Sebagai contoh, saya akan menggunakan Jetty untuk front-end testing secara otomatis.  Proyek saya berjalan pada Cloud Foundry yang memakai Tomcat 6.  Dengan demikian, versi Jetty yang paling mendekati adalah Jetty 7 yang masih memakai spesifikasi Servlet 2.5 sama seperti Tomcat 6.  Sebagai informasi, Jetty sudah mencapai versi 9 yang mendukung spesifikasi Servlet 3.0 seperti Tomcat 7.

Karena memakai Apache Maven, saya dapat menambahkan dependency ke Jetty secara mudah.  Agar mudah, saya menambahkan seluruh JAR yang dibutuhkan (versi aggregate), dengan informasi seperti berikut ini:

Group Id:  org.eclipse.jetty.aggregate
Artifact Id: jetty-all
Version: 7.6.7.v20120910
Scope: test

Disini saya memakai scope test,  yang menunjukkan bahwa artifact JAR Jetty hanya dibutuhkan untuk pengujian saja, bukan untuk menjalankan aplikasi saya.

Saya juga memastikan bahwa servlet-api dan jsp-api sudah ada di dependency Maven:

Group Id: javax.servlet
Artifact Id: servlet-api
Version: 2.5
Scope: provided

Group Id: javax.servlet.jsp
Artifact Id: jsp-api
Version: 2.1
Scope: provided

Saya memakai scope provided, yang menunjukkan bahwa kedua artifact tersebut dibutuhkan untuk melakukan kompilasi kode program, tetapi tidak perlu disertakan sebagai bagian dari proyek karena mereka telah disediakan oleh server.

Dan terakhir, karena implementasi JSP yang saya pakai adalah Jasper,  saya perlu menambahkan artifact Juli (untuk logging di Tomcat) pada dependency Maven:

Group Id: org.apache.tomcat
Artifact Id: juli
Version: 6.0.36
Scope: test

Setelah memastikan  Maven telah men-download semua artifact JAR yang dibutuhkan, saya bisa membuat kode program.  Sebagai contoh, berikut ini adalah penggalan kode program yang menjalankan Jetty sebagai embedded web server:

...
Server server = new Server (8080);
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);
server.setHandler(context);
server.start();
...

Semua proyek web Java selalu memiliki file web.xml.  Yang saya lakukan adalah memberitahu lokasi web.xml yang akan dijalankan pada Jetty.  Selain itu, saya juga memberi tahu lokasi folder yang berisi file HTML, JSP, dan sebagainya (resource base).  Dengan setContextPath(“/proyek”) maka Jetty dapat dipanggil dengan URL seperti http://localhost:8080/proyek.

Sekarang, bila kode program di atas selesai dikerjakan, saya dapat langsung membuka http://localhost:8080/proyek tanpa perlu meng-install  server seperti Tomcat atau GlassFish.

Proyek lain yang cukup menarik adalah Cargo (informasi ada di http://cargo.codehaus.org).  Dengan Cargo, saya bisa memberikan deskripsi server yang akan saya pakai  (misalnya Tomcat, Jetty, GlassFish, dan sebagainya) di kode program.  Nantinya, Cargo yang akan men-download dan men-install server yang dibutuhkan bila belum ada di komputer dimana program saya berjalan, kemudian Cargo akan memindahkan hasil build program secara otomatis.  Jadi bila saya ingin beralih dari Tomcat ke GlassFish, saya hanya perlu mengubah deskripsi server yang saya pakai.  Contoh kegunaan Cargo adalah pada server Continous Integration (CI).  Server CI akan men-checkout hasil kerja masing-masing programmer yang telah digabungkan, lalu menjalankan function testing secara otomatis.  Server CI bekerja secara periodik dan otomatis sehingga bila ditemui kesalahan, developer akan secepat mungkin mendapat notifikasi.