Menghindari CORS saat memakai Spring Boot bersama dengan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek baru yang menggabungkan back end dan front end. Untuk menjalankan back end, saya memberikan perintah ./gradlew bootRun yang akan membuat instance server Tomcat baru. Saya bisa meng-eksekusi end points milik back end di http://localhost:8080/api. Setiap kali kode back end berubah, server ini akan diperbaharui. Sementara itu, untuk menjalankan front end, saya memberikan perintah yarn dev yang juga akan menjalankan server baru di http://localhost:9090. Setiap kali kode front end berubah, server ini akan diperbaharui.

Apa yang salah bila server front end berbeda dengan back end? Hampir semua browser modern sudah mendukung fasilitas Cross-origin Resource Sharing (CORS) yang akan memblokir akses end points milik front end dari domain berbeda. Tentunya saya tidak ingin setiap API milik back end saya bisa diakses oleh front end milik orang lain sesuka hati. Akan tetapi, saat mengembangkan kode program, server front end dan back end adalah dua server berbeda. Fasilitas CORS akan membuat front end saya gagal memanggil back end. Memang Spring Boot memiliki annotation @CrossOrigin yang bisa membuat back end saya dipakai semua orang sesuka hati sehingga front end saya yang berada di server berbeda bisa memanggilnya. Akan tetapi, ini membuat saya merasa kurang aman.

Apakah ada solusi lain? Yup! Saya beruntung karena template webpack yang dipakai oleh vue-cli sudah memperhitungkan hal ini. Bila saya memperhatikan isi file dev-server.js yang dijalankan oleh yarn dev, terlihat bahwa http-proxy-middleware sudah disertakan. Proyek ini pada dasarnya adalah sebuah HTTP proxy (memakai http-proxy) berbasis JavaScript. Dengan http-proxy-middleware, saya ingin pada saat front end mengakses http://localhost:9090/api, secara transparan, ia akan memanggil http://localhost:8080/api. Karena proxy ini transparan dari sisi browser, saya tidak akan menemukan masalah CORS lagi.

Untuk itu, saya akan mengubah file config/index.js pada bagian dev.proxyTable sehingga terlihat seperti berikut ini:

module.exports = {
  ...
  dev: {
    ...
    proxyTable: {
      '/api': 'http://localhost:8080'
    }
  }
}

Pada konfigurasi di atas, setiap kali saya mengakses url yang diawali oleh /api di server front end seperti http://localhost:9090/api/snakes, akses tersebut akan diteruskan ke server back end di http://localhost:8080/api/snakes secara transparan. Dengan demikian, saya tidak akan mendapatkan kesalahan CORS lagi.

Memakai Bootstrap bersama dengan Vue.js dan Webpack

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat sebuah website kosong yang menggunakan Vue.js dan berbasis Webpack. Salah satu hal menarik yang saya sukai dari Vue.js adalah setiap komponen (single file component) memiliki deklarasi CSS-nya masing-masing. Agar CSS hanya berlaku untuk sebuah komponen (dan juga komponen lain yang dikandunginya), saya tinggal menambahkan <style scoped>. Selain itu, saya juga bisa menggunakan CSS Module dengan menggunakan <style module>.

Walaupun deklarasi CSS per komponen adalah hal yang baik, akan tetapi, ada saatnya saya juga membutuhkan CSS yang global untuk seluruh bagian di website. Sebagai contoh, akan sulit sekali bagi saya untuk menulis ulang CSS seperti Bootstrap grid di setiap komponen. Bagaimana cara menambahkan Bootstrap pada proyek Vue.js yang berbasis Webpack?

Satu hal yang perlu saya pertimbangkan adalah komponen JavaScript di Bootstrap masih menggunakan jQuery sementara filosofi Vue.js yang reaktif membuat saya ingin menghindari jQuery sebisa mungkin. Beruntungnya, saya menemukan https://bootstrap-vue.js.org yang menyediakan Bootstrap 4 dimana komponen-nya telah ditulis ulang sebagai komponen Vue.js yang tidak menggunakan jQuery. Untuk menggunakan library tersebut, saya segera menambahkan baris berikut ini pada package.json:

"dependencies": {
    ...
    "bootstrap-vue": "^0.18.0"
},

Library ini akan menyertakan bootstrap@^4.0.0-alpha.6 yang berisi Bootstrap 4. Mengapa tidak mengubah file index.html dan menambahkan baris seperti <link rel="stylesheet" href="..."> dan <script src="...">? Salah satu alasannya adalah akan lebih konsisten bila seluruh dependensi proyek diletakkan pada file package.json. Bila saya ingin memakai versi Bootstrap yang berbeda, misalnya, saya hanya perlu memperbaharui package.json tanpa mengubah index.html.

Berikutnya, saya menambahkan baris berikut ini pada awal file main.js:

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Proyek Webpack yang dihasilkan oleh vue-cli sudah dilengkapi konfigurasi yang memakai css-loader (lihat file buildScripts\util.js) sehingga saya bisa men-import file css di dalam kode program JavaScript.

Berikutnya, saya perlu mengaktifkan bootstrap-vue dengan menambahkan kode program berikut ini di main.js:

Vue.use(BootstrapVue)

Sekarang, bila saya menjalankan website ini dengan yarn run dev, saya akan menemukan bahwa CSS Bootstrap (dan juga CSS lain yang saya import dikemudian hari) akan diletakkan secara inline ke dalam tag <style> seperti yang terlihat pada gambar berikut ini:

Inline CSS pada saat `run dev`

Inline CSS pada saat `run dev`

Walaupun CSS inline lebih sederhana, ia tidak disarankan untuk dipakai di lingkungan produksi. Beruntungnya, proyek Webpack yang dihasilkan oleh vue-cli sudah memperhitungkan hal ini. Pada file buildScripts\util.js, terlihat bahwa tersedia option extract yang akan menggunakan plugin extract-text-webpack-plugin untuk menghasilkan file CSS terpisah. Seperti yang bisa ditebak, extract akan bernilai true bila perintah ini dikerjakan pada lingkungan produksi (lihat file buildScripts/vue-loader.conf.js). Untuk membuktikannya, saya akan menjalankan perintah yarn run build. Output dari perintah ini dapat dijumpai di folder dist. Kali ini, CSS akan diletakkan pada file terpisah seperti yang terlihat pada gambar berikut ini:

File CSS terpisah saat memakai `yarn run build`

File CSS terpisah saat memakai `yarn run build`

Memperbaharui Output Webpack Di Browser Secara Otomatis

Pada artikel Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama, saya membuat proyek Spring Boot yang menggunakan Webpack di front end. Webpack adalah bundler yang juga berfungsi melakukan transformasi konten. Sebagai contoh, Webpack akan melakukan transpiling kode program ES2016 saya ke dalam kode program JavaScript ‘lama’ yang bisa dimengerti semua browser berkat babel-loader. Selain itu, saya bisa menulis style dalam format Scss (Sassy CSS) yang kemudian akan diterjemahkan oleh sass-loader menjadi CSS biasa yang bisa dimengerti oleh semua browser. Semua hasil dari Webpack ini akan disimpan kedalam sebuah Jar.

Apa yang terjadi bila saya menjalankan Spring Boot dengan gradlew bootRun lalu mengubah kode program HTML atau JavaScript yang ada? Hasil perubahan ini tidak akan terlihat pada browser! Mengapa demikian? Karena gradlew bootRun hanya memanggil Webpack pada saat pertama kali dijalankan. Saat saya mengubah kode program di HTML atau JavaScript, Webpack tidak akan dipanggil. Hal ini tentu akan sangat merepotkan karena saya perlu menjalankan gradlew bootRun berkali-kali setiap kali melakukan perubahan di front end.

Beruntungnya, konfigurasi Webpack yang dihasilkan vue-cli sudah mendukung live reload. Template yang saya pakai memiliki script buildScripts/dev-server.js yang menggunakan fasilitas dari webpack-dev-middleware untuk menghasilkan live reload. Ini hampir sama seperti apa yang dilakukan oleh webpack-dev-server. Bila saya membuka file package.json, terlihat bahwa scripts dev atau start akan melakukan hal yang sama yaitu mengerjakan buildScripts/dev-server.js.

Agar bisa memanggil script ini secara mudah, saya bisa menambahkan kode program berikut ini pada build.gradle (milik proyek front end):

...
task runDev(dependsOn: yarn, type: YarnTask) {
    args = ['run', 'dev']
}
...

Sekarang, saya bisa menjalankan proyek front end (tanpa harus menjalankan Spring Boot) dengan menggunakan perintah berikut ini:

$ ./gradlew runDev
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runDev
yarn run v0.27.5
$ node buildScripts/dev-server.js
> Starting dev server...
 DONE  Compiled successfully in 1904ms00:08:40

> Listening at http://localhost:8080

 75% EXECUTING
> :frontend:runDev

Perintah di atas akan membuka browser yang menampilkan halaman front end saya. Sekarang, bila saya melakukan perubahan pada HTML atau JavaScript, halaman tersebut akan langsung diperbaharui secara otomatis. Saya juga bisa menemukan output seperti berikut ini di console:

 WAIT  Compiling...00:00:12

 DONE  Compiled successfully in 154ms00:00:13

Sekarang, saya bisa mengerjakan front end dan melihat hasilnya secara cepat.

Bagaimana bila saya ingin menjalankan Spring Boot dan dev-server.js secara bersamaan? Karena keduanya sama-sama menggunakan port 8080, salah satu akan gagal. Untuk itu, saya akan menggubah port yang dipakai oleh dev-server.js menjadi 9090 dengan mengubah port di config/index.js.

Menyatukan Frontend dan Backend Pada Satu Proyek Yang Sama

Salah satu pertanyaan yang sering muncul pada saat akan memulai proyek baru dari awal adalah apakah kode program front end dan back end harus dipisahkan menjadi dua proyek yang berbeda? Proyek front end umumnya adalah proyek berbasis JavaScript yang menggunakan framework seperti Angular, Vue.js, React, dan sebagainya. Build tool yang umumnya digunakan oleh proyek front end meliputi Grunt, Yarn, Webpack, npm, dan sejenisnya. Sementara itu, proyek back end umumnya ditulis dalam Java, PHP, C#, dan sebagainya. Build tool yang umum meliputi Gradle, Ant, Composer, CMake, dan sebagainya. Karena dunia keduanya yang berbeda jauh, apakah pemisahan proyek secara fisik selalu merupakan pilihan yang tepat?

Tidak selamanya front end dan back end harus dipisahkan menjadi dua proyek berbeda. Bila anggota tim lebih banyak yang full stack, pemisahan proyek justru malah akan merepotkan. Metodologi pengembangan software seperti Scrum, misalnya, memiliki unit pekerjaan berupa story seperti “pelanggan bisa mendaftar baru” dan “pelanggan bisa melihat laporan laba rugi per tahun”. Agar bisa menyelesaikan story yang sedang dikerjakannya, developer umumnya harus mengubah front end dan juga back end.

Beberapa framework yang dogmatis mempermudah penggabungan kode program front end dan back end pada satu proyek yang sama. Sebagai contoh, Laravel 5.4 yang dilengkapi dengan Laravel Mix (berbasis Webpack) sangat mempermudah penggunaan front end Vue.js di dalam satu proyek Laravel yang sama. Lalu bagaimana dengan framework di dunia Java seperti Spring Boot? Walaupun tidak semudah di Laravel, saya akan mencoba membuat proyek Spring Boot (back end) + Webpack + Vue.js (front end) pada artikel ini.

Saya akan mulai dengan membuat sebuah proyek Spring Boot baru melalui Spring Initializr (https://start.spring.io). Setelah menjalankan IntelliJ IDEA, saya segera memilih New, Project…. Setelah memilih Spring Initializr, saya men-klik tombol Next. Karena saya ingin menggunakan Gradle, saya mengisi dialog yang muncul seperti yang terlihat pada gambar berikut ini:

Membuat Proyek Spring Boot Baru

Membuat Proyek Spring Boot Baru

Pada langkah berikutnya, saya memilih dependency yang diperlukan. Karena ini merupakan sebuah percobaan, saya hanya memberikan tanda centang pada Web. Setelah men-klik Next, saya bisa mengisi nama proyek dan lokasi penyimpanannya. Setelah men-klik Finish, IDEA akan men-download proyek dan menampilkan sebuah proyek Spring Boot yang masih kosong seperti yang terlihat pada gambar berikut ini:

Struktur Proyek Spring Boot Yang Baru Dibuat

Struktur Proyek Spring Boot Yang Baru Dibuat

Seandainya saja saya menggunakan front end yang ramah terhadap Java seperti Thymeleaf dan JSF, maka saya hanya perlu meletakkan kode program mereka ke direktori /src/main/webapp. Akan tetapi, ada kalanya saya perlu menggunakan front end yang lebih berat seperti Angular dan Vue.js. Keuntungannya adalah saya bisa memanfaatkan teknologi yang umum dipakai programmer front end seperti npm, Yarn, dan Webpack. Walaupun pada dunia Java sudah ada upaya menjadikan library front end ke dalam apa yang disebut sebagai Webjars, saya tetap merasa penggunaan npm lebih alami bagi pencinta JavaScript.

Bagaimana cara memasukkan proyek Vue.js ke dalam proyek Spring Boot yang baru saja dibuat ini? Cara yang paling adalah dengan menggunakan Vue Cli (https://github.com/vuejs/vue-cli). Jika ini adalah pertamakalinya saya menggunakan Vue Cli, saya perlu men-install-nya dengan memberikan perintah berikut ini:

$ npm install -g vue-cli

Setelah itu, masih berada di direktori proyek Spring Boot, saya memberikan perintah berikut ini:

$ vue init webpack frontend

Vue Cli akan menghasilkan sebuah proyek kosong untuk Vue.js berbasis Webpack seperti yang terlihat pada gambar berikut ini:

Proyek Vue.js di dalam proyek Spring Boot

Proyek Vue.js di dalam proyek Spring Boot

Perlu diperhatikan bahwa frontend hanyalah sebuah folder biasa. Sampai disini, saya bisa saja mulai bekerja dengan Vue.js dan memberikan perintah npm pada saat berada dalam folder frontend. Walaupun demikian, akan lebih baik bila saya bisa sama-sama mengendalikan front end dan back end melalui Gradle. Berhubung Gradle mendukung multi-projects, saya akan menjadikan folder frontend sebagai sebuah sub-project.

Langkah pertama yang saya lakukan adalah membuat sebuah file baru bernama build.gradle di folder frontend dengan isi seperti berikut ini:

plugins {
  id 'com.moowork.node' version '1.2.0'
  id 'java'
}

repositories {
  mavenCentral()
}

node {
  version = '8.1.3'
  yarnVersion = '0.27.5'
  download = true
}

task runBuild(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'build']
}

task runDev(dependsOn: yarn, type: YarnTask) {
  args = ['run', 'dev']
}

jar {
  from project.projectDir.absolutePath + '/dist'
  eachFile { details ->
    details.path = details.path.startsWith('META-INF')?: 'static/' + details.path
  }
  includeEmptyDirs = false
}

jar.dependsOn runBuild

Pada file di atas, saya menggunakan plugin com.moowork.node yang akan men-download Node.js dan Yarn ke dalam folder .gradle. Sebagai informasi, Yarn adalah sebuah klien npm yang masih bisa menggunakan file package.json seperti klien npm standard. Salah satu kelebihan Yarn adalah ia memiliki penyimpanan global di folder ~/.cache/yarn (sama halnya dengan Gradle yang memiliki folder ~/.gradle). Bila saya membuat proyek baru yang memakai library yang sudah di-download sebelumnya, Yarn tidak akan membutuhkan koneksi internet lagi karena ia akan memakai yang sudah tersimpan di ~/.cache/yarn (proses build pun terasa lebih cepat karena tidak perlu men-download ulang). Ini berbeda dengan cache klien npm standard (~/.npm) yang hanya sementara dan tidak permanen. Karena Yarn kompatibel dengan klien npm standard, saya bisa mengganti perintah npm run build dengan yarn run build.

Spring Boot secara otomatis akan menyajikan file yang berada di folder /public dan /static sehingga bisa diakses oleh pengguna. Oleh sebab itu, file Gradle di atas akan menyalin seluruh file yang ada di folder dist ke dalam folder static di dalam file Jar yang dihasilkan nantinya.

Salah satu masalah yang saya jumpai pada proyek Vue.js yang dihasilkan oleh vue-cli adalah build scripts yang diletakkan di folder build. Hal ini akan menimbulkan sedikit kekacauan karena folder build memiliki arti berbeda di Gradle yang akan digunakan sebagai tempat output (misalnya file Jar yang dihasilkan Gradle akan diletakkan di folder ini). Oleh sebab itu, saya mengganti nama folder build menjadi buildScripts. Tidak lupa juga, saya men-update file package.json supaya menggunakan nama folder yang baru.

Berikutnya, pada proyek utama (Spring Boot), saya menambahkan sebuah file baru bernama settings.gradle yang isinya berupa:

rootProject.name = 'latihan'
include 'frontend'

Sebagai langkah terakhir, saya menambahkan dependency ke subproyek frontend di file build.gradle milik proyek utama, seperti pada:

dependencies {
   compile project(':frontend')
   ...
}

Sekarang, bila saya melihat daftar perintah Gradle yang bisa dijalankan untuk proyek ini, saya akan menjumpai seperti yang terlihat pada gambar berikut ini:

Daftar perintah Gradle untuk proyek ini

Daftar perintah Gradle untuk proyek ini

Mari jalankan proyek ini dengan men-double click bootRun dibagian application atau dengan memberikan perintah berikut ini:

$ ./gradlew bootRun
Starting a Gradle Daemon (subsequent builds will be faster)
:frontend:compileJava NO-SOURCE
:frontend:processResources NO-SOURCE
:frontend:classes UP-TO-DATE
:frontend:nodeSetup UP-TO-DATE
:frontend:yarnSetup UP-TO-DATE
:frontend:yarn UP-TO-DATE
:frontend:runBuild
yarn run v0.27.5
$ node buildScripts/build.js

Starting to optimize CSS...
Processing static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css...
Processed static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css, before: 431, after: 363, ratio: 84.22%
Hash: 424b3c987da691d210b2ING
Version: webpack 2.6.1ECUTING
Time: 5139msrunBuild
                                                  Asset       Size  Chunks             Chunk Names
                  static/js/app.eaedbed942638ee1e5dd.js    11.7 kB       0  [emitted]  app
               static/js/vendor.4e561224e1f68ab595ac.js     107 kB       1  [emitted]  vendor
             static/js/manifest.4373dcd698dd3ea60ed7.js    1.51 kB       2  [emitted]  manifest
    static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css  363 bytes       0  [emitted]  app
              static/js/app.eaedbed942638ee1e5dd.js.map    36.7 kB       0  [emitted]  app
static/css/app.c6d9c9fc12c1dbaee77703a4dd731a8b.css.map  910 bytes       0  [emitted]  app
           static/js/vendor.4e561224e1f68ab595ac.js.map     857 kB       1  [emitted]  vendor
         static/js/manifest.4373dcd698dd3ea60ed7.js.map    14.6 kB       2  [emitted]  manifest
                                             index.html  445 bytes          [emitted]  

  Build complete.

  Tip: built files are meant to be served over an HTTP server.
  Opening index.html over file:// won't work.

Done in 9.22s.
:frontend:jar UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:findMainClass
:bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.4.RELEASE)

2017-07-10 20:07:40.825  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Starting LatihanApplication on desktop with PID 4098 (/home/user/IdeaProjects/latihan/build/classes/main started by user in /home/userIdeaProjects/latihan)
2017-07-10 20:07:40.828  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : No active profile set, falling back to default profiles: default
2017-07-10 20:07:40.913  INFO 4098 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.200  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-07-10 20:07:42.213  INFO 4098 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2017-07-10 20:07:42.215  INFO 4098 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.15
2017-07-10 20:07:42.364  INFO 4098 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-07-10 20:07:42.366  INFO 4098 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1456 ms
2017-07-10 20:07:42.478  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-07-10 20:07:42.482  INFO 4098 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-07-10 20:07:42.776  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@5f71c76a: startup date [Mon Jul 10 20:07:40 WIB 2017]; root of context hierarchy
2017-07-10 20:07:42.839  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-07-10 20:07:42.841  INFO 4098 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-07-10 20:07:42.867  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.868  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.902  INFO 4098 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-07-10 20:07:42.922  INFO 4098 --- [           main] oConfiguration$WelcomePageHandlerMapping : Adding welcome page: class path resource [static/index.html]
2017-07-10 20:07:43.017  INFO 4098 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-07-10 20:07:43.074  INFO 4098 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-07-10 20:07:43.081  INFO 4098 --- [           main] com.example.latihan.LatihanApplication   : Started LatihanApplication in 2.57 seconds (JVM running for 3.034)
<============-> 92% EXECUTING
> :bootRun

Bila perintah ini dikerjakan pertama kalinya, ia akan men-download Node.js dan Yarn. Selain itu, ia juga akan men-download library di package.json yang belum ada di folder node_modules. Setelah itu, ia akan menjalankan Webpack yang secara tidak langsung bekerja melalui loader-nya. Sebagai contoh, vue-loader akan bekerja menerjemahkan komponen Vue dalam file .vue dan babel-loader akan melakukan transpiling sehingga saya bisa memakai ES2016 tanpa khawatir tidak bisa dijalankan oleh browser-browser populer. Setelah selesai, saya bisa membuka http://localhost:8080 untuk mengakses aplikasi.

Sampai disini, saya sudah membuat proyek baru yang menggabungkan kode program front end dan back end. Saya juga memakai Gradle untuk mengendalikan kode program front end (yang secara tidak langsung akan memakai Yarn dan Webpack yang dipakai oleh proyek front end).

Bagaimana dengan deployment? Bila saya memberikan perintah:

$ ./gradlew war

Saya akan memperoleh sebuah file War bernama latihan-0.0.1-SNAPSHOT.war di lokasi build/libs. File ini dapat langsung di-deploy di application server produksi (misalnya Tomcat atau JBoss). Di dalam file War ini sudah termasuk proyek front end saya yang bisa ditemukan di WEB-INF\lib\frontend.jar. Jadi, Gradle akan membuat Jar untuk front end terlebih dahulu. Setelah itu, Jar akan dipaketkan bersama-sama dengan Jar lain yang dibutuhkan oleh back end di WEB-INF\lib.

Apa Bedanya Adobe Illustrator Dengan Photoshop?

Pada suatu hari, saya meminta mahasiswa untuk merancang gambar yang dipakai pada pemograman game. Saya menyarankan mereka untuk menggunakan Adobe Illustrator. Seorang mahasiswa pencinta Photoshop kontan protes dan segera membela tool kesayangannya. Mengapa memakai Illustrator? Berbeda dengan Photoshop, Illustrator menyimpan data dalam bentuk vector sehingga scalable. Bila pengguna Photoshop cenderung menggunakan brush untuk mempoles gambar, maka pengguna Illustrator lebih sering memanipulasi bentuk (shape dan anchor point). Hal ini membuat Illustrator sangat nyaman dipakai untuk merancang gambar seperti kartun dan icon.

Pada Illustrator, terdapat 2 tool pemilihan utama yang sering digunakan, yaitu Selection Tool (V) dan Direct Selection Tool (A) seperti yang terlihat pada gambar berikut ini:

Selection dan Direct Selection Tool

Selection dan Direct Selection Tool

Dengan Direct Selection Tool, saya bisa memilih setiap titik (disebut sebagai anchor point) pada sebuah bentuk. Dengan demikian, saya bisa memanipulasi setiap anchor point yang ada. Sebagai contoh, saya bisa mengubah sebuah persegi panjang menjadi trapesium:

Memodifikasi anchor point

Memodifikasi anchor point

Saya juga bisa melakukan hal di atas dengan Photoshop! Untuk menghapus atau menambah anchor point di Photoshop, saya dapat memilih tool yang terletak di kotak yang sama dengan Pen Tool (P). Selain itu, Direct Selection Tool (A) di Photoshop tersembunyi karena terdapat di kotak yang sama dengan Path Selection Tool (A).

Illustrator memiliki menu khusus untuk memanipulasi path yang dapat diakses di Object, Path. Sebagai contoh, tidak ada cara cepat untuk membuat shape segitiga di versi Photoshop yang saya pakai. Sebaliknya, membuat segitiga sangat mudah di Illustrator. Saya mulai dengan membuat sebuah persegi panjang dan memilih anchor point di sudut kiri atas dan kanan atas. Setelah itu, seusai memakai menu Object, Path, Average…, saya akan memperoleh sebuah segitiga seperti pada gambar berikut ini:

Membuat segitiga

Membuat segitiga

Sama seperti di Photoshop, saya juga dapat mengatur stroke dan mengisi sebuah bentuk dengan gradien di Illustrator. Sebagai contoh, saya akan menghapus stroke dan mengisi bentuk trapesium yang saya buat dengan gradien seperti pada gambar berikut ini:

Mengisi trapesium dengan gradien

Mengisi trapesium dengan gradien

Satu hal yang tidak bisa saya lakukan di Photoshop secara cepat adalah menghaluskan ujung (rounding) pada trapesium ini. Photoshop memang menyediakan pengaturan penghalusan ujung untuk persegi panjang, tapi setelah dimodifikasi menjadi trapesium, saya tidak menemukan pengaturan tersebut lagi. Di Illustrator, saya dapat melakukan hal ini cukup dengan memilih menu Effect, Stylize, Round Corners… seperti pada gambar berikut ini:

Menghaluskan ujung trapesium

Menghaluskan ujung trapesium

Saya kemudian membuat dua trapesium lain di samping kiri dan kanan sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat trapesium baru di sisi kiri dan kanan

Membuat trapesium baru di sisi kiri dan kanan

Sekarang, saya ingin sebuah trapesium di bagian bawah untuk melengkapi bagian yang kosong. Pada Photoshop, saya harus menggambarnya secara manual. Illustrator menawarkan cara lain melalui penggunaan Shape Builder Tool (Shift+M). Dengan Shape Builder Tool, saya bisa menggabungkan atau menghapus (dengan menahan tombol Alt) bagian kombinasi dari beberapa shape. Sebagai contoh, saya membuat sebuah persegi panjang dan meletakkannya di belakang (dengan memilih Object, Arrange, Send To Back) seperti pada gambar berikut ini:

Menambahkan sebuah persegi panjang

Menambahkan sebuah persegi panjang

Setelah itu, saya memilih seluruh shape yang ada, kemudian membuang bagian yang tidak dibutuhkan dengan menggunakan Shape Builder Tool:

Memakai shape builder tool

Memakai shape builder tool

Setiap shape di Illustrator bisa memiliki lebih dari satu fill atau stroke dengan effect-nya masing-masing, seperti yang terlihat pada gambar berikut ini:

Lebih dari 1 fill pada shape yang sama

Lebih dari 1 fill pada shape yang sama

Hal serupa dapat dicapai di Photoshop dengan cara yang lebih rumit, misalnya dengan membuat beberapa fill layer yang dikombinasikan dengan layer mask sehingga hanya berlaku untuk shape yang dikehendaki (bukan bagian gambar lainnya).

Langkah berikutnya, saya menambahkan sebuah persegi panjang baru sehingga gambar saya terlihat seperti pada gambar berikut ini:

Membuat persegi panjang baru

Membuat persegi panjang baru

Untuk memberikan ujung yang baru pada bagian bawah persegi panjang, saya dapat menggunakan Direct Selection Tool untuk memilih kedua anchor point di ujung kiri bawah dan kanan bawah, kemudian mengisi nilai corners seperti pada gambar berikut ini:

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Menghaluskan hanya ujung kiri bawah dan kanan bawah

Hal serupa juga dapat dilakukan di Photoshop dengan cara yang berbeda, yaitu dengan mengisi nilai rounding di window Properties.

Bila pengguna Photoshop seringkali memoles gambar dengan menggunakan brush, maka pengguna Illustrator memoles kartun dengan potongan shape melalui fasilitas Pathfinder. Sebagai contoh, saya bisa menduplikasi persegi panjang yang baru dibuat sebanyak dua kali. Saya kemudian menggeser salah satu persegi panjang ke kanan sebanyak 1 piksel. Setelah itu, saya memilih kedua persegi panjang dan memilih Minus Front dari window Pathfinder untuk memperoleh sebuah shape baru seperti pada gambar berikut ini:

Memakai pathfinder

Memakai pathfinder

Saya kemudian melakukan hal yang sama untuk membuat shape di ujung kanan. Setelah itu, saya bisa memberikan gradiasi berbeda pada shape baru yang terbentuk di ujung kiri dan kanan, seperti pada gambar berikut ini:

Gradiasi berbeda di ujung sisi  kiri dan kanan

Gradiasi berbeda di ujung sisi kiri dan kanan

Karena hasil akhir di Illustrator adalah vector, saya tidak perlu khawatir gambar menjadi rusak saat diperbesar atau diperkecil. Vector aman untuk di-resize. Sebagai contoh, pada hasil akhir berikut ini, gambar dapat ditampilkan dengan baik pada beberapa ukuran yang berbeda:

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 256 x 256 piksel

Gambar dengan ukuran 128 x 128  piksel

Gambar dengan ukuran 128 x 128 piksel

Gambar dengan ukuran 64 x 64  piksel

Gambar dengan ukuran 64 x 64 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 48 x 48 piksel

Gambar dengan ukuran 32 x 32 piksel

Gambar dengan ukuran 32 x 32 piksel

Melakukan Pengujian jQuery Ajax Di QUnit

Sesuai dengan namanya, unit testing adalah pengujian yang dilakukan pada satuan atau unit terkecil dari kode program. Unit lainnya sebisa mungkin dianggap konstan atau tidak berubah. Dengan demikian, bila terjadi kegagalan pengujian pada sebuah unit, developer bisa yakin bahwa penyebab kegagalan terjadi pada unit tersebut (bukan pada unit lain yang mungkin dipanggil). Untuk membuat unit lain menjadi konstan, developer dapat menerapkan teknik seperti mocking dan stubbing.

Sebagai contoh, anggap saja saya perlu menguji kode program JavaScript seperti berikut ini:

function simpan(data, callback) {      
  $.ajax({
    url: 'simpan',
    type: 'POST',
    data: JSON.stringify(data),
    contentType: 'application/json; charset=utf-8',
    success: function(hasil) {
      if (hasil.ok) {
        data.id = hasil.id;
      }
      callback();          
    }
  });      
}

Function di atas akan memakai $.ajax dari jQuery untuk mengirim sebuah object yang hendak disimpan dalam bentuk JSON ke server. Setelah nilai dikembalikan dari server secara asynchronous, maka function callback() akan dikerjakan dimana argumen hasil berisi informasi dari server. Bila proses penyimpanan sukses, maka nilai hasil.ok adalah true. Selain itu, atribut id dari object akan di-isi dengan sebuah nilai yang dihasilkan secara otomatis oleh server. Bila server gagal melakukan penyimpanan, maka nilai hasil.ok adalah false.

Untuk memastikan apakah kode program yang saya buat sudah memenuhi skenario yang ditetapkan, saya perlu membuat pengujian QUnit dalam bentuk sebuah file HTML, misalnya seperti berikut ini:

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Latihan</title>  
  <script src="jquery.js"></script>  
  <link rel="stylesheet" href="qunit.css" media="screen">  
  <script src="qunit.js"></script>      
  <script>
    function simpan(data, callback) {      
      $.ajax({
        url: 'simpan',
        type: 'POST',
        data: JSON.stringify(data),
        contentType: 'application/json; charset=utf-8',
        success: function(hasil) {
          if (hasil.ok) {
            data.id = hasil.id;
          }
          callback();          
        }
      });      
    }

    QUnit.test('simpan(data) sukses', function(assert) {
      var mahasiswa = { nama: 'Solid Snake', usia: 27 };
      simpan(mahasiswa, function(hasil) {
        assert.equal(hasil.ok, true, 'Nilai hasil.ok harus true bila penyimpanan sukses')
        assert.ok(mahasiswa.id !== undefined, 'Nilai id harus terisi');  
      });            
    });

    QUnit.test('simpan(data) gagal', function(assert) {
      var mahasiswa = { nama: 'Solid Snake', usia: 27 };
      simpan(mahasiswa, function(hasil) {
        assert.equal(hasil.ok, false, 'Nilai hasil.ok harus false bila penyimpanan gagal');
        assert.ok(mahasiswa.id === undefined, 'Nilai id harus masih kosong');        
      });            
    });
  </script>    
</head>
<body>  
  <div id="qunit"></div>  
</body>
</html>

Akan tetapi, bila saya menjalankan pengujian dengan membuka file HTML tersebut di browser, saya akan menemukan pesan kegagalan seperti pada gambar berikut ini:

Pengujian yang gagal

Pengujian yang gagal

Hal ini terjadi karena request Ajax tidak dilakukan sehingga callback() tidak pernah dipanggil. Untuk membuat pengujian sukses, saya perlu memperoleh hasil kembalian dari server. Akan tetapi karena ini adalah unit test, saya tidak ingin ada pemanggilan ke server secara nyata pada saat pengujian dilakukan. Oleh sebab itu, saya perlu mensimulasikan kembalian dari server.

Beruntungnya, karena JavaScript adalah bahasa yang dinamis, melakukan stubbing secara mudah bukanlah hal yang mustahil. Salah satu cara yang paling mudah adalah mengganti method jQuery.ajax() dengan method yang langsung memanggil success. Sebagai contoh, saya bisa mengubah test case saya menjadi seperti berikut ini:

QUnit.test('simpan(data) sukses', function(assert) {      
  var mahasiswa = { nama: 'Solid Snake', usia: 27 };
  jQuery.ajax = function(args) {
    args.success({ok: true, id: 999});
  } 
  simpan(mahasiswa, function(hasil) {
    assert.equal(hasil.ok, true, 'Nilai hasil.ok harus true bila penyimpanan sukses')
    assert.ok(mahasiswa.id !== undefined, 'Nilai id harus terisi');  
  });            
});

QUnit.test('simpan(data) gagal', function(assert) {
  var mahasiswa = { nama: 'Solid Snake', usia: 27 };
  jQuery.ajax = function(args) {
    args.success({ok: false});
  }
  simpan(mahasiswa, function(hasil) {
    assert.equal(hasil.ok, false, 'Nilai hasil.ok harus false bila penyimpanan gagal');
    assert.ok(mahasiswa.id === undefined, 'Nilai id harus masih kosong');        
  });            
});

Pada kasus sukses, saya membuat seolah-olah server mengirimkan respon berupa {ok: true, id: 999}. Pada kasus gagal, saya menganggap server mengirimkan respon berupa {ok: false}. Sekarang, bila saya menjalankan pengujian, semuanya akan sukses seperti pada gambar berikut ini:

Pengujian sukses setelah men-stub method jQuery.ajax()

Pengujian sukses setelah men-stub method jQuery.ajax()

Melakukan Front-End Testing Dengan Codeception

Pada artikel Melakukan Front-End Testing Dengan Selenium Tanpa “Mengotori” Database, saya mencoba melakukan pengujian halaman web secara otomatis. Pengujian dilakukan dengan mengisi database dengan data yang konsisten lalu mensimulasikan akses pada browser dan memastikan hasilnya sesuai harapan. Pada artikel ini, saya akan melakukan hal yang sama tetapi dengan menggunakan Codeception. Codeception adalah sebuah ‘framework’ pengujian yang mempermudah menulis kode program pengujian. Dukungan yang dimiliki oleh Codeception ditawarkan dalam bentuk module seperti Selenium2, Webdriver, PhpBrowser, dan lainnya.

Sebagai latihan, saya akan menguji sebuah web sederhana dimana terdapat sebuah halaman yang hanya bisa ditampilkan setelah pengguna login. Proyek web tersebut dibuat dengan menggunakan fasilitas Auth yang disediakan oleh Laravel.

Untuk men-install Codeception pada proyek Laravel, saya akan menggunakan perintah Composer berikut ini:

C:\proyek> composer require "codeception/codeception:*"

Composer akan men-download Codeception pada folder vendor/codeception.

Langkah pertama untuk memakai Codeception adalah menghasilkan file konfigurasi dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept bootstrap app

Perintah di atas akan menyebabkan Codeception membuat beberapa file baru yang dibutuhkan oleh Codeception di folder app\tests. Folder acceptance, functional dan unit masing-masing mewakili jenis pengujian. Istilah yang dipakai untuk front-end testing disini adalah acceptance testing. Ini yang akan saya pakai nantinya.

Sebelumnya, saya perlu mencari file file acceptance.suite.yml dan mengubah nilai url agar sesuai dengan lokasi URL aplikasi yang hendak diuji. Ini adalah salah satu konfigurasi yang dibutuhkan oleh PHPBrowser. Secara default, Codeception akan memakai module PHPBrowser untuk front-end testing. Karena aplikasi yang saya uji tidak mengandung JavaScript, maka ini adalah pilihan yang masuk akal. Bila hendak menguji interaksi melalui JavaScript pada aplikasi, saya harus memakai module Selenium2.

Untuk membuat sebuah file pengujian baru, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:cept acceptance Login -c app

Perintah di atas akan membuat sebuah file baru bernama LoginCept.php di folder app\tests\acceptance. Saya dapat membuat kode program yang mewakili pengujian di file ini. Sebagai contoh, saya dapat mengubah isinya menjadi seperti berikut ini:

<?php
$I = new WebGuy($scenario);

$I->wantTo('display home page');
$I->amOnPage('home/welcome');
$I->dontSee('Selamat datang');
$I->see('Login');

$I->wantTo('login');
$I->fillField('nama', 'snake');
$I->fillField('password', '12345');
$I->click('Login');
$I->amOnPage('home/welcome');
$I->see('Selamat datang, snake');
$I->seeLink('logout');

Kode program di atas sangat mudah dipahami, bukan? Untuk menjalankannya, saya akan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept run

Saya akan memperoleh hasil seperti berikut ini:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (1) ------------------------
Trying to login (LoginCept.php)         Ok
---------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 858 ms, Memory: 6.50Mb

OK (1 test, 4 assertions)

Salah masalah dengan pendekatan skenario seperti di atas adalah saya harus memiliki 1 file PHP per skenario. Sebagai contoh, walaupun pada kode program pengujian, saya memiliki 2 buah wantTo(), hasil pengujian hanya menampilkan yang terakhir kali saja (Trying to login). Untuk mengatasi hal seperti ini, saya dapat mendeklarasi pegujian dalam bentuk class. Untuk itu, saya membuat skenario baru dengan memberikan perintah seperti berikut ini:

C:\proyek> vendor\bin\codecept generate:cest acceptance Auth -c app

Perhatikan bahwa pada perintah di atas, saya memakai generate:cest sementara pada perintah sebelumnya, saya memakai generate:cept. Perintah tersebut akan menghasilkan sebuah file baru bernama AuthCest.php di folder acceptance. File tersebut berisi class AuthenticationCest yang akan saya ubah menjadi seperti berikut ini:

<?php
use WebGuy;

class AuthCest
{

    public function visit(WebGuy $I) {
        $I->wantTo('display home page as guest');
        $I->amOnPage('home/welcome');
        $I->dontSee('Selamat datang');
        $I->see('Login');
    }

    /**     
     * @before login
     */
    public function wrongLogin(WebGuy $I) {
        $I->wantTo('login with wrong password');
        $I->lookForwardTo('see error when entering wrong password');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', 'wrong password');
        $I->click('Login');                
        $I->seeInField('Nama', 'snake');
        $I->see('Terjadi kegagalan');      
    }

    public function login(WebGuy $I) {
        $I->wantTo('login');
        $I->amOnPage('login/show');
        $I->fillField('nama', 'snake');
        $I->fillField('password', '12345');
        $I->click('Login');
        $I->amOnPage('home/welcome');
        $I->see('Selamat datang, snake');
        $I->seeLink('logout');
    }

    /**     
     * @after login
     */
    public function logout(WebGuy $I) {
        $I->wantTo('logout');
        $I->seeLink('logout');
        $I->click('logout');
        $I->dontSee('Selamat datang');
        $I->see('Nama');
        $I->see('Password');
        $I->see('Login');
    }

}

Bila saya menjalankan pengujian, kali ini saya akan memperoleh hasil seperti:

Codeception PHP Testing Framework v1.8.5
Powered by PHPUnit 3.7.37 by Sebastian Bergmann.

Acceptance Tests (4) ------------------------------------------------
Trying to display home page as guest (AuthCest.visit)           Ok
Trying to login with wrong password (AuthCest.wrongLogin)       Ok
Trying to login (AuthCest.login)                                Ok
Trying to logout (AuthCest.logout)                              Ok
---------------------------------------------------------------------

Functional Tests (0) ------------------------
---------------------------------------------

Unit Tests (0) ------------------------------
---------------------------------------------


Time: 1.59 seconds, Memory: 6.75Mb

OK (4 tests, 15 assertions)

Selain menjalankan pengujian, saya juga bisa mengubah kode program pengujian menjadi sesuatu yang lebih mudah dibaca (oleh mereka yang bukan developer) dengan memberikan perintah berikut ini:

C:\proyek> vendor\bin\codecept generate:scenarios acceptance -c app

Hasilnya dapat dilihat pada folder app/tests/_data/scenarios/acceptance. Saya akan menemukan file seperti Auth_Cest.login.txt, Auth_Cest.logout.txt, dan sebagainya. Sebagai contoh, bila saya melihat isi file Auth_Cest.login.txt, saya akan menemukan hasil seperti berikut ini:

I WANT TO LOGIN

I am on page "login/show"
I fill field "nama","snake"
I fill field "password","12345"
I click "Login"
I am on page "home/welcome"
I see "Selamat datang, snake"
I see link "logout"

Unit Testing Di JavaScript Dengan QUnit

Pada artikel ini, saya akan mencoba menggunakan QUnit untuk melakukan unit testing pada kode program JavaScript. Berbeda dengan Java dimana unit adalah sebuah class, pada JavaScript, unit adalah sebuah function. Salah satu tool yang dapat dipakai untuk melakukan unit testing pada JavaScript adalah QUnit. Tool ini juga dipakai untuk melakukan pengujian pada proyek jQuery dan jQuery UI. Untuk memakai QUnit, saya dapat men-download-nya di http://qunitjs.com.

Sebagai latihan, saya akan membuat sebuah library sederhana yang mengimplementasikan fungsi require() seperti pada spesifikasi module oleh CommonJS. JavaScript (sebelum ES6) tidak memiliki konsep module. Oleh sebab itu, developer JavaScript seringkali mengalami masalah berkaitan dengan penggunaan banyak library berbeda dan kesalahan penamaan yang sama. Pada Java, hal ini diatasi melalui konsep package dan keyword import. ECMAScript 6 nanti juga akan memperkenalkan konsep module pada JavaScript. Saat ini, untuk sementara ada beberapa spesifikasi yang dapat dipakai dalam mengatur kode program JavaScript agar lebih standar dalam mengelola module. Salah satunya adalah yang dibuat oleh CommonJS (http://www.commonjs.org/specs/modules/1.0/). Yang ditawarkan adalah koleksi design pattern yang bisa diterapkan oleh setiap developer JavaScript. Karena ini hanya latihan, saya tidak akan membuat sesuatu yang mengikuti seluruh yang ada di spesifikasi (selain itu, implementasi CommonJS sudah banyak, salah satunya adalah Node.js).

Kali ini saya akan membuat unit test dengan mengikuti filosofi test driven development (TDD). Dengan TDD, saya mulai dengan membuat test case terlebih dahulu baru kemudian membuat implementasi kode programnya. Yup! Membuat unit test dulu baru membuat kode program! Saya pernah mendengar cerit bahwa seorang dosen S2 yang sedang mendidik dosen S1 guna sertifikasi mengatakan bahwa harus ada GUI (tampilan atau output) terlebih dahulu baru bisa menguji kode program. Tentu saja ia salah. Lalu, bagaimana cara mengujinya?

Saya akan mulai dengan membuat test case yang mengikuti spesifikasi CommonJS dengan membuat 2 file JavaScript yang mewakili module, yaitu moduleA.js dan moduleB.js. Isi dari moduleA.js adalah:

exports.foo = require("moduleB").foo;

Sementara itu, isi dari moduleB.js adalah:

exports.foo = function() {};

Pada spesifikasi CommonJS, fungsi require() dipakai untuk menyertakan modul lain. Sebuah modul yang ingin men-export function atau nilai agar dapat dipakai oleh modul lainnya harus mengisinya pada exports yang selalu tersedia. Pada contoh di atas, moduleA akan memakai moduleB dimana function foo() dari moduleA persis sama seperti function foo() dari moduleB.

Berikutnya, saya akan membuat sebuah halaman HTML sederhana yang mewakili sebuah test case dengan nama test.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Menguji module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA!=undefined, "moduleA harus punya nilai");
            ok(moduleB!=undefined, "moduleB harus punya nilai");
        });
        test("Menguji hasil export di module", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");
            ok(moduleA.foo!=undefined, "moduleA.foo harus ada");          
            ok(moduleB.foo!=undefined, "moduleB.foo harus ada");
        });
        test("Menguji function yang di-export", function() {
            var moduleA = require("moduleA");
            var moduleB = require("moduleB");     
            equal(moduleA.foo, moduleB.foo);
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menjalankan HTML di atas, saya akan menemui tampilan seperti pada gambar berikut ini:

Hasil awal pengujian menunjukkan semua test gagal

Hasil awal pengujian menunjukkan semua test gagal

Jangan kaget! Di TDD, pada awalnya seluruh test akan gagal. Tujuan dari TDD adalah bagaimana membuat test yang gagal tersebut menjadi benar dan sukses (ini adalah arti kata test-driven dimana pemicu pembuatan kode program adalah test case). Progress atau perkembangan proyek juga bisa dilihat berdasarkan jumlah test yang sukses atau gagal.

Sebagai langkah berikutnya, saya akan membuat sebuah file JavaScript baru bernama myrequire.js yang mewakili kode program yang hendak diuji dengan isi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        return module;
    }       

    global.require = require;
}(this));

Saya kemudian menambahkan baris berikut ini pada test.html:

<script src="myrequire.js"></script>

Sekarang, bila saya menampilkan file tersebut pada browser, akan ada 1 test case yang sukses, namun yang lainnya masih gagal, seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama sudah sukses

Hasil pengujian menunjukkan test case pertama sudah sukses

Saya kembali melakukan perubahan di myrequire.js agar test case berikutnya menjadi sukses. Sebagai contoh, saya mengubahnya menjadi seperti berikut ini:

(function(global) {
    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    global.require = require;
}(this));

Sekarang, bila saya menjalankan pengujian QUnit, saya akan memperoleh hasil seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Hasil pengujian menunjukkan test case pertama dan kedua sudah sukses

Sudah semakin baik bukan? Hanya saja, agar semua test sukses, kode program saya perlu membaca file JavaScript yang mewakili module. Untuk itu, saya perlu memperkenalkan sebuah function baru yang membaca file JavaScript dengan menggunakan XHR. Seperti biasa, pada TDD, sebelum membuat kode program, saya terlebih dahulu membuat test case-nya. Untuk itu, saya membuat file testBacaFile.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', bacaFile("moduleB.js"));
        });             
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                bacaFile("entahDimana.js");
            });
        });
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Pada pengujian di atas, saya memastikan bahwa bacaFile() akan mengembalikan string yang sesuai dengan isi file JavaScript. Bila saya berusaha membaca file yang tidak ada, function tersebut harus mengembalikan sebuah exception. Pada QUnit, saya menggunakan throws() untuk memeriksa apakah ada exception yang dikembalikan (dan saya juga bisa memeriksa jenis exception yang dikembalikan bila perlu).

Seperti biasa, bila saya menjalankan pengujian di atas, saya akan menemukan kegagalan karena function bacaFile() belum ada. Oleh sebab itu, saya perlu membuatnya dengan mengubah kode program myrequire.js menjadi seperti berikut ini:

(function(global) {

    function require(moduleId) {            
        var module = {};
        module.foo = function() {};
        return module;
    }       

    function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    };

    global.require = require;
}(this));

Saya kemudian kembali menjalankan unit test di testBacaFile.html, tapi saya kembali mendapatkan pesan kesalahan yang sama bahwa bacaFile is not defined. Mengapa demikian? Hal ini karena function bacaFile() berada dalam closure sehingga tidak dapat diakses dari luar (termasuk oleh test case). Saya tidak bisa menguji sesuatu yang tidak bisa saya akses. Untuk itu, saya akan mengganti penggunakan closure menjadi sesuatu yang lebih OOP dengan mengubah kode program di myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {};
        module.foo = function() {};
        return module;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", namaFile, false); 
        return xhr.responseText;        
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Walaupun cara diatas menyebabkan ada variabel global myRequire, setidaknya ini akan mempermudah pengujian yang merupakan kriteria penting dalam TDD. Sekarang, saya bisa mengubah kode program di testBacaFile.html menjadi seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" />
    <script src="qunit-1.14.0.js"></script>
    <script src="myrequire.js"></script>
    <script>
        test("Membaca module dalam direktori yang sama", function() {         
            equal('exports.foo = require("moduleB").foo;', myRequire.bacaFile("moduleA.js"));
            equal('exports.foo = function() {};', myRequire.bacaFile("moduleB.js"));
        }); 
        test("Membaca module yang tidak ada", function() {
            throws(function() {
                myRequire.bacaFile("entahDimana.js")
            });
        });         
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Tapi bila saya menampilkan HTML tersebut, kode program saya masih salah. File masih belum dibaca dengan benar. Mengapa demikian? Frustasi adalah hal biasa bagi developer. Setidaknya TDD membantu saya mengetahui bahwa ada yang salah dengan kode program sebelum library ini di-integrasi-kan pada proyek web yang lebih kompleks lagi.

Saya akhir menemukan bahwa saya lupa memanggil xhr.send()! Pantas saja gagal, saya segera menambahkan xhr.send() setelah xhr.open() di function bacaFile(). Sekarang, bila saya menampilkan testBacaFile.html, maka test case pertama akan sukses seperti pada gambar berikut ini:

Hasil pengujian menunjukkan test pertama sudah sukses

Hasil pengujian menunjukkan test pertama sudah sukses

Berikutnya, bagaimana caranya agar test case kedua tidak gagal? Saya harus men-throw sesuatu bila terjadi kegagalan saat membaca file. Sebagai contoh, saya mengubah kode program bacaFile() di myrequire.js menjadi seperti berikut ini:

...
bacaFile: function bacaFile(namaFile) {
    var xhr = new XMLHttpRequest();     
    xhr.open("GET", namaFile, false);
    xhr.send();
    if (xhr.status===200) {
        return xhr.responseText;
    } else {
        throw "Can't read file: " + namaFile;
    }       
},
...

Sekarang, seluruh pengujian untuk method bacaFile() akan sukses seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Dampak dari unit test adalah membuat saya lebih percaya diri. Saya setidaknya bisa yakin bahwa bacaFile() sudah bekerja sesuai dengan yang diharapkan.

Perkembangannya terasa dengan jelas, bukan? Sampai disini saya bisa istirahat sejenak, menikmati segelas teh hangat atau jalan-jalan sebentar. Setelah saya kembali, saya tahu apa yang harus dilakukan. Masih ada test case yang gagal 🙂

Agar semua test case di test.html sukses, saya mengubah kode program pada myrequire.js menjadi seperti berikut ini:

function MyRequire(exports) {
    this.exports = exports;
    this.modules = new Map();
};

MyRequire.prototype = {

    require: function require(moduleId) {           
        var module = {id: moduleId, exports: {}};           

        // TIdak melakukan loading bila sudah pernah di-load    
        if (this.modules.has(moduleId)) return this.modules.get(moduleId).exports;

        // Membaca file JS yang mewakili module
        new Function("module", "exports", this.bacaFile(moduleId + ".js"))
            .call(this.exports, module, module.exports);
        this.modules.set(moduleId, module);                     

        return module.exports;
    },

    bacaFile: function bacaFile(namaFile) {
        var xhr = new XMLHttpRequest();     
        xhr.open("GET", namaFile, false);
        xhr.send();
        if (xhr.status===200) {
            return xhr.responseText;
        } else {
            throw "Can't read file: " + namaFile;
        }       
    },

}

this.myRequire = new MyRequire(typeof exports === 'object' && exports || this);
this.require = function(s) { return this.myRequire.require(s); };

Sekarang, bila saya menjalankan kode program, seluruh test yang ada akan sukses seperti pada yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Sebagai informasi, QUnit juga mendukung pola module di CommonJS sehingga saya dapat memanggilnya melalui require(). Sebagai contoh, saya akan membuat unit test lagi yang diambil dari halaman dokumentasi CommonJS di http://www.commonjs.org/specs/modules/1.0/. Saya membuat file math.js yang isinya seperti berikut ini (sesuai dengan example di website tersebut):

exports.add = function() {
    var sum = 0, i = 0, args = arguments, l = args.length;
    while (i < l) {
        sum += args[i++];
    }
    return sum;
}

Kemudian, saya membuat file increment.js yang isinya seperti berikut ini (sesuai dengan example di website CommonJS):

var add = require('math').add;
exports.increment = function(val) {
    return add(val, 1);
};

Setelah itu, saya akan membuat unit test di file testSample.html yang isinya seperti berikut ini:

<!doctype html>
<html>
<head>
    <title>Latihan QUnit</title>
    <link rel="stylesheet" href="qunit-1.14.0.css" /> 
    <script src="myrequire.js"></script>
    <script>
        var QUnit = require('qunit-1.14.0');

        test('Sample Common JS', function() {
            var inc = require('increment').increment;
            var a = 1;
            equal(2, inc(a));
            var b = 5;
            equal(6, inc(b));
        });             
    </script>
</head>
<body>
    <div id="qunit"></div>
</body>
</html>

Bila saya menampilkan halaman di atas pada browser, saya akan memperoleh tampilan yang menunjukkan bahwa kode program saya bekerja sesuai harapan seperti yang terlihat pada gambar berikut ini:

Hasil pengujian menunjukkan seluruh test case sudah sukses

Hasil pengujian menunjukkan seluruh test case sudah sukses

Cara di atas, dimana saya membuat unit test setelah selesai membuat kode program, adalah sesuatu yang melanggar prinsip test driven development (TDD). Pada TDD, saya harus membuat atau mengembangkan unit test terlebih dahulu sebelum membuat kode program. Tapi tentu saja menambah unit test belakangan bukan sesuatu yang salah. Semakin banyak test case yang ada akan membuat sebuah proyek menjadi semakin teruji dan handal.

Memakai Grunt Untuk Mengelola Proyek JavaScript

Saya sudah biasa memakai Gradle atau Maven untuk mengelola proyek Java. Lalu bagaimana dengan proyek JavaScript? Tentu saja tool serupa tidak begitu berguna bila dipakai pada web yang hanya sekedar memakai JavaScript di dalam HTML. Tapi untuk sebuah proyek library JavaScript (seperti jQuery), penggunaan tool otomatis untuk mengelola proyek akan sangat berguna. Grunt adalah salah satu tool yang dirancang untuk keperluan tersebut. Contoh proyek open-source JavaScript yang memakai Grunt adalah jQuery. Pada artikel ini, saya akan mencoba memakai Grunt untuk mengelola proyek JavaScript sederhana.

Untuk men-install Grunt, saya perlu memakai npm. npm adalah package manager yang berjalan pada platform Node.js. Ini adalah sesuatu yang memiliki fungsi mirip seperti Composer di PHP. Pada platform Linux, saya memberikan perintah berikut ini untuk men-install npm:

$ sudo apt-get install nodejs
$ sudo apt-get install npm

Setelah npm ter-install, saya dapat men-install Grunt CLI dengan menggunakan perintah berikut ini:

$ sudo npm install -g grunt-cli

Perintah di atas akan men-install perintah grunt-cli pada lokasi /usr/local (dapat diatur dengan mengubah nilai konfigurasi prefix). Dengan demikian, saya dapat memanggil perintah grunt dari mana saja.

Untuk menghasilkan template proyek JavaScript secara cepat, saya akan menggunakan grunt-init. Tapi sebelumnya, saya perlu men-install-nya terlebih dahulu dengan memberikan perintah berikut ini:

$ sudo npm install -g grunt-init

Untuk memakai grunt-init, saya perlu men-download minimal sebuah template yang akan dijadikan sebagai patokan struktur direktori awal. Lokasi template secara default terletak di folder ~/.grunt-init/. Sebagai latihan, saya akan memakai template grunt-init-commonjs dengan memberikan perintah berikut ini:

$ git clone https://github.com/gruntjs/grunt-init-commonjs.git ~/.grunt-init/commonjs

Bila git belum ter-install, saya perlu memberikan perintah sudo apt-get install git. Perintah di atas akan menyalin sebuah template untuk proyek JavaScript yang bersifat umum.

Saya siap untuk membuat proyek baru. Tapi sebelumnya, saya perlu mengatasi sebuah permasalahan kecil terlebih dahulu. Pada distro Linux yang saya pakai, perintah seperti grunt atau grunt-init tidak akan bisa dijalankan, malah muncul kesalahan seperti berikut ini:

$ grunt --version
/usr/bin/env: node: No such file or directory

Hal ini terjadi karena konflik nama antara package untuk Node.js dengan package node (Amateur Packet Radio Node Program) sehingga binary Node.js yang seharusnya adalah node terpaksa mengalah dan diganti nama menjadi nodejs. Karena banyak script yang mengharapkan nama binary Node.js berupa node, saya perlu me-rename nodejs menjadi node, atau agar aman, saya dapat membuat symbolic link seperti berikut ini:

$ sudo ln -s /usr/bin/nodejs /usr/bin/node

Sekarang, saya siap untuk membuat sebuah proyek JavaScript baru dengan memberikan perintah berikut ini:

$ mkdir myUtils
$ cd myUtils
$ grunt-init commonjs
Running "init:commonjs" (init) task
This task will create one or more files in the current directory, based on the
environment and the answers to a few questions. Note that answering "?" to any
question will show question-specific help and answering "none" to most questions
will leave its value blank.

Please answer the following:
[?] Project name (myUtils) 
[?] Description (The best project ever.) My reusable JS APIs.
[?] Version (0.1.0) 
[?] Project git repository (git://github.com/snake/myUtils.git) 
[?] Project homepage (https://github.com/snake/myUtils) 
[?] Project issues tracker (https://github.com/snake/myUtils/issues) 
[?] Licenses (MIT) Apache
[?] Author name (none) Solid Snake
[?] Author email (none) solid@snake.com
[?] Author url (none) https://thesolidsnake.wordpress.com
[?] What versions of node does it run on? (>= 0.10.0) 
[?] Main module/entry point (lib/myUtils) 
[?] Npm test command (grunt nodeunit) 
[?] Do you need to make any changes to the above before continuing? (y/N) N

Writing .gitignore...OK
Writing .jshintrc...OK
Writing Gruntfile.js...OK
Writing README.md...OK
Writing lib/.jshintrc...OK
Writing lib/myUtils.js...OK
Writing test/myUtils_test.js...OK
Writing package.json...OK

Initialized from template "commonjs".
You should now install project dependencies with npm install. After that, you
may execute project tasks with grunt. For more information about installing
and configuring Grunt, please see the Getting Started guide:

http://gruntjs.com/getting-started

Done, without errors.

grunt-init akan menanyakan beberapa pertanyaan. Saya bisa mengisinya atau menerima nilai default dengan menekan tombol Enter. Setelah pertanyaan selesai dijawab, grunt-init akan membuat sebuah proyek baru dengan struktur seperti berikut ini:

$ tree -a
.
|--- .gitignore
|--- Gruntfile.js
|--- .jshintrc
|--- lib
|    |--- .jshintrc
|    |--- myUtils.js
|--- package.json
|--- README.md
|--- test
     |--- myUtils_test.js

2 directories, 8 files

File Gruntfile.js adalah file wajib yang dibutuhkan untuk bekerja dengan Grunt. File package.json dipakai oleh npm untuk men-download modul lain yang dibutuhkan. grunt-init juga sudah membuat file lib\myUtils.js yang nantinya akan berisi kode program saya. Selain itu, juga ada file lib\myUtils_test.js yang berisi unit test untuk menguji program JavaScript yang ada.

Bila saya membuka file package.json, saya akan memperoleh isi "devDependencies" yang terlihat seperti berikut ini:

...
"devDependencies": {
    "grunt-contrib-concat": "~0.3.0",
    "grunt-contrib-uglify": "~0.2.0",
    "grunt-contrib-jshint": "~0.6.0",
    "grunt-contrib-nodeunit": "~0.2.0",
    "grunt-contrib-watch": "~0.4.0",
    "grunt": "~0.4.5"
},
...

Konfigurasi di atas menunjukkan ketergantungan pada beberapa plugin Grunt. Salah satu kelebihan Grunt adalah ia memiliki banyak plugin seperti yang terdaftar di http://gruntjs.com/plugins. Sebagai contoh, grunt-contrib-concat adalah plugin Grunt untuk menggabungkan beberapa file JavaScript yang berbeda menjadi satu. Developer selalu lebih nyaman memakai beberapa file berbeda (misalnya satu file untuk sebuah object JavaScript) sementara kinerja download akan lebih baik bila <script> merujuk pada satu file tunggal. Itu sebabnya file perlu digabungkan menjadi satu pada saat distribusi. Plugin grunt-contrib-uglify akan memakai UglifyJS untuk menghasilkan versi minified dari kode program JavaScript yang memiliki ukuran lebih kecil. Plugin grunt-contrib-jshint akan memakai JSHint untuk melakukan analisa kode program kode program JavaScript (JSHint adalah fork dari JSLint). Plugin grunt-contrib-nodeunit dibutuhkan untuk melakukan unit test dengan menggunakan nodeunit (ini adalah sesuatu yang memiliki fungsi mirip seperti JUnit di Java). Dan terakhir, plugin grunt-contrib-watch memiliki kemampuan untuk mengerjakan task Grunt tertentu secara otomatis bila ada file yang berubah.

Saat ini, semua plugin yang dibutuhkan belum ter-install pada lokasi proyek. Oleh sebab itu, saya perlu meminta npm untuk men-download semua yang ada di package.json tersebut dengan memberikan perintah:

$ npm install

Perintah di atas akan menyebabkan npm men-download dan meletakkan file yang dibutuhkan pada folder node_modules.

Berikutnya, saya akan melihat is file Gruntfile.js. Secara garis besar, saya dapat melihat konfigurasi untuk masing-masing task yang ada seperti berikut ini:

'use strict';

module.exports = function(grunt) {


  grunt.initConfig({

    pkg: ...,

    banner: ...,

    concat: ...,

    uglify: ...,

    nodeunit: ...,

    jshint: ...,

    watch: ...,

  });

  ...

};

Selain konfigurasi, saya juga menjumpai pemanggilan grunt.loadNpmTasks() yang akan me-load plugin. Setiap plugin menawarkan task masing-masing. Bila menginginkan task yang tidak disediakan oleh plugin Grunt, saya dapat membuat kode programnya sendiri dengan memanggil grunt.registerTask() yang melewatkan function yang berisi apa yang akan dikerjakan oleh task baru tersebut.

Apa itu task? Task adalah sebuah proses yang dikerjakan secara otomatis. Pada era konvensional, developer membuat batchfile (*.bat) atau shell script (*.sh) untuk mengerjakan proses pengelolaan proyek secara otomatis. Tapi file-file tersebut sering kali menjadi bertambah banyak, tidak terorganisir, dan sulit dipakai oleh developer lain (perbedaan platform, bahasa, dsb). Apache Ant mempopulerkan cara baru yang lebih seragam dan lebih standar dimana task yang umum dijumpai pada pengelolaan proyek Java didefinisikan dalam bentuk XML. Kesuksesan Ant pun dilanjutkan oleh tool lain seperti Maven dan Gradle (yang kini sedang ‘naik daun’ dan dipakai oleh Android Studio). Grunt adalah salah satu tool serupa tetapi ditujukan untuk mengelola proyek JavaScript.

Untuk melihat task apa saja yang bisa dikerjakan, saya dapat memberikan perintah berikut ini:

$ grunt --help
...
Available tasks
        concat  Concatenate files. *                                           
        uglify  Minify files with UglifyJS. *                                  
      nodeunit  Run Nodeunit unit tests. *                                     
        jshint  Validate files with JSHint. *                                  
         watch  Run predefined tasks whenever watched files change.            
       default  Alias for "jshint", "nodeunit", "concat", "uglify" tasks. 
...

Pada bagian Available tasks, saya dapat menjumpai task apa saja yang dapat saya panggil.

Sebagai contoh, bila saya ingin menjalankan unit test, saya dapat memberikan perintah seperti berikut ini:

$ grunt nodeunit
Running "nodeunit:files" (nodeunit) task
Testing myUtils_test.js.OK
>> 1 assertions passed (10ms)

Done, without errors.

Terlihat bahwa hasil pengujian sukses tanpa kesalahan.

Untuk menghasilkan file distribusi dimana seluruh file JavaScript terpisah akan digabungkan menjadi satu, saya dapat memberikan perintah:

$ grunt concat
Running "concat:dist" (concat) task
File "dist/myUtils.js" created.

Done, without errors.

Task di atas akan membuat sebuah file baru di folder dist dengan nama myUtils.js. Ini adalah file yang dapat didistribusikan kepada pengguna. Saya bisa membuat versi minified dari file yang dihasilkan oleh task concat tersebut dengan memberikan perintah:

$ grunt uglify
Running "uglify:dist" (uglify) task
File "dist/myUtils.min.js" created.

Done, without errors.

File myUtils.min.js adalah file berukuran kecil dari file myUtils.js yang dihasilkan oleh UglifyJS.

Penggunaan template grunt-init membuat struktur proyek JavaScript menjadi jelas dan rapi. Sebagai contoh, pada template yang saya pakai, kode program JavaScript yang dibuat developer terletak di direktori lib. Hasil akhir yang merupakan penggabungan file di lib dan versi minified-nya dapat dijumpai di folder dist. Kedua file tersebut merupakan output yang dapat didistribusikan langsung ke pengguna.

Task watch adalah sebuah task yang agak unik yang tidak saya jumpai di dunia Java. Bila saya menjalankan task ini, saya akan memperoleh hasil seperti:

$ grunt watch
Running "watch" task
Waiting...

Saya harus tetap membiarkan console ini tetap aktif. Lalu, saya membuka console lain dan melakukan perubahan pada file lib/myUtils.js. Begitu saya selesai menyimpan perubahan pada file tersebut, console yang menjalankan task watch akan menampilkan informasi seperti berikut ini:

Running "watch" task
Waiting...OK
>> File "lib/myUtils.js" changed.

Running "jshint:lib" (jshint) task
>> 1 file lint free.

Running "nodeunit:files" (nodeunit) task
Testing myUtils_test.jsF
>> awesome - no args
>> Message: should be awesome.
>> Error: 'not awesome?' == 'awesome'
>> at Object.exports.awesome.no args (test/myUtils_test.js:33:10)
>> at Object.exports.awesome.setUp (test/myUtils_test.js:28:5)

Warning: 1/1 assertions failed (17ms) Use --force to continue.

Waiting...

Terlihat bahwa begitu ada file JavaScript yang dimodifikasi oleh developer, maka task jshint dan nodeunit akan dikerjakan secara otomatis dan saya bisa langsung melihat laporannya. Bila proses seperti ini dilakukan di server tempat dimana seluruh developer men-commit perubahan kode program mereka, maka saya sudah memiliki sebuah sistem continous integration sederhana yang menguji kode program dari developer secara otomatis setiap kali ada perubahan.

Mencoba ECMAScript 6 Harmony Di Firefox

Apa itu ECMAScript? Tidak lain adalah JavaScript! Lalu kenapa disebut ECMAScript? Berdasarkan riwayat di http://en.wikipedia.org/wiki/ECMAScript, Netscape adalah yang pertama kali memasarkan bahasa scripting bernama JavaScript pada browser Netscape Navigator (cikal bakal Firefox). Karena JavaScript sangat sukses, Microsoft kemudian mengembangkan bahasa serupa yang diberi nama JScript untuk dipakai di Internet Explorer. Microsoft kemudian melakukan standarisasi JScript melalui badan standarisasi Ecma International. Setelah berbagai perlawanan antar Netscape (kini menjadi Mozilla Organization) dan Microsoft, akhirnya bahasa yang telah di-standarisasi tersebut, ECMAScript, disepakati menjadi milik bersama. Jadi, JavaScript dan JScript disepakati sebagai implementasi dari bahasa ECMAScript.

Mungkin saja bagi pihak Mozilla, JScript seharusnya adalah implementasi dari JavaScript dan ECMAScript tidak perlu ada, karena mereka yang terlebih dahulu menciptakan JavaScript 🙂 Browser Internet Explorer sebenarnya memakai JScript (http://en.wikipedia.org/wiki/JScript) bukan JavaScript. Microsoft juga membuat varian JScript yang disebut JScript .NET. Tapi bagi sebagian besar developer web, JScript di Internet Explorer dianggap sama seperti JavaScript. Dan yang lebih membingungkan lagi, Microsoft merombak implementasi ECMAScript 5 di Windows 8 dan kini menyebutnya sebagai JavaScript.

Versi paling baru ECMAScript adalah versi 6 yang disebut juga ES6 Harmony. ES6 saat ini belum final, tapi beberapa browser termasuk Firefox sudah mengimplementasikan beberapa bagian dari proposalnya. Seperti apa bahasa pemograman JavaScript generasi terbaru nanti? Saya akan mencoba beberapa fitur yang sudah diimplementasikan oleh Firefox.

ES6 mendukung spread operator. Pada Groovy, ini dilakukan dengan menggunakan operator bintang. Pada ES6, spreading dilakukan dengan menggunakan tiga titik (…) seperti pada contoh berikut ini:

function tambah(a,b) {
    return a + b;
}
var angka = [10, 20];
console.log(tambah(...angka)); // output: 30
// setera dengan:
console.log(tambah(angka[0], angka[1]));  // output: 30

Pada contoh di atas, setiap elemen dari array akan diterjemahkan menjadi argumen.

Saya dapat membuat sebuah variabel menjadi konstan (tidak dapat diubah nilainya) dengan menggunakan const seperti berikut ini:

const JUMLAH_KOLOM = 5;  // variabel ini tidak boleh diubah

ES6 juga memiliki arrow function untuk mendeklarasikan anonymous function secara lebih singkat lagi, seperti pada contoh berikut ini:

tombol.onclick = (e) => { alert('test'); }
// setara dengan:
tombol.onclick = function(e) { alert('test'); }

ES6 juga membolehkan definisi nilai default untuk parameter pada sebuah function seperti pada contoh berikut ini:

function cetak(pesan = '') {
    console.log(pesan + '<br>');
}
cetak(); // output: <br>

Bandingkan hasilnya dengan tanpa nilai default seperti:

function cetak(pesan) {
    console.log(pesan + '<br>');
}
cetak(); // output: undefined<br>

ES6 juga memiliki struktur data baru seperti Map, WeakMap dan Set. Sebagai contoh, kode program berikut ini memakai Map:

var daftarNilai = new Map();
daftarNilai.set('Solid Snake', 60);
daftarNilai.set('Liquid Snake', 70);
for (var[nama, nilai] of daftarNilai) {
    console.log(nama + ' memiliki nilai ' + nilai);
}

// Output:
// Solid Snake memiliki nilai 60
// Liquid Snake memiliki nilai 70

ES6 juga memiliki banyak fitur lainnya yang menarik namun belum di-implementasi-kan oleh Firefox. Salah satunya adalah adanya class. Dengan adanya class, maka OOP di JavaScript bisa lebih mudah lagi dibandingkan dengan saat ini yang berbasis prototype. Selain itu, ES6 juga akan mendukung module sehingga nantinya akan ada keyword import.