How to Integration Test (Versi Laravel)
“Quality is not an act, it is a habit.” — Aristotle
Resolusi 2017 di Javan adalah meningkatkan quality software yang kami produksi. Salah satu yang dilakukan divisi Dev kami adalah mewajibkan software yang dideliver memiliki acceptable-coverage automated test.
Tulisan ini sebenarnya adalah materi yang akan di workshop kan yang kemudian berubah jadi sebuah tulisan. Part pertama membahas opinionated tehnik integration test pada Laravel.
Studi Kasus
Twitter Clone
Features:
- Create account
- Post a tweet
- View another’s user tweets
- Follow another user
- View my timeline tweets
- View my own tweet list
Macam-macam test
Mari kita sepahamkan dulu arti dari banyak definisi yang beredar seputar automated test.
Unit test: programmer. PHPUnit, JUnit
ciri-ciri:
- Isolated. Kita ngetest method dengan mengabaikan hubungannya dengan class lain (dengan Mock) atau external resources (database, third party service).
- Fastest execution time karena isolated tadi.
- Sulit karena cenderung harus banyak ngeMock.
Integration test: programmer. PHPUnit, JUnit
ciri-ciri:
- Tidak isolated, bisa berhubungan dengan external resources
- Lebih lama execution time daripada unit test
- Cenderung lebih mudah dibuat daripada unit test
Functional / End to End test / Acceptance test: programmer, tester. Selenium
ciri-ciri:
- Ngetest bagaikan kita ngetest seperti user yang menggunakan browser
- Waktu Eksekusi sangat lama
Mock: Membuat semacam fake method/function untuk mensimulasikan suatu method/function dengan output yang kita define sendiri.
Contoh macam-macam test
Andaikan kita punya class Post
dengan method Store
seperti dibawah
Bagaimana cara kita ngetest jika menggunakan:
Unit test
Menurutku percuma kita ngetest itu dengan pendekatan unit test karena Post
itu bergantung pada external resources yaitu database, maka kita harus ngemock Post
.
Jika kita ngemock Post
, maka kita define output dari Post
itu apa sehingga tidak mencerminkan fungsionalitasi sebenarnya method tersebut.
Integration test
Panggil method store($title, $content)
dengan params $title
dan $content
yang telah kita define.
Method store()
akan create row baru di table post
sehingga kita bisa memvalidasi apakah ada data di table post
dengan title yang telah kita define tadi.
Functional / End to End / Acceptance test
Harus masuk ke halaman create Post lalu bikin script pakai selenium untuk ngisi form, kita define isi dari field title dan content.
Setelah klik submit, validasi dengan script selenium apakah masuk ke halaman List Post dan menampilkan tulisan `Create post success!`
Kali ini kita akan fokus hanya pada Integration Test
Mindset
- Programmer harus bertanggungjawab terhadap kode yang ia buat.
- Maka dari itu, ia buat test (Integration test) untuk memastikan bahwa kode yang dia buat berjalan baik.
- Test (Integration test) yang dia buat bukan untuk mencari bug.
Aturan Test
- Test harus mudah dibaca
- Fitur harus bisa diketahui kegunaannya hanya dari membaca test
- Wajib menggunakan 3 Step untuk readibility
- Setup
- Exercise
- Verify
Studi Kasus
Jika disuruh membuat twitter clone dengan fitur create a tweet, gimana cara bikin test untuk fitur itu?
Pertama, bikin fiturnya dulu
Ada dua pendekatan untuk ngetest fitur create tweet
- Test method
createTweet($tweet)
nya saja - Panggil routes nya method
store()
Pendekatan 1
- Pada bagian Setup, test menyiapkan data yang dibutuhkan untuk test agar bisa ditest. Dalam hal ini, untuk bisa membuat tweet, maka perlu ada user nya dulu.
- Step Exercise adalah memanggil method yang ingin ditest. Params dari method yang ingin ditest kita define sendiri.
- Lalu pada step Verify, kita validasi apakah method yang kita panggil tadi sudah melakukan tugasnya dengan benar. Dalam hal ini, ketika method
createTweet()
dipanggil, maka seharusnya terdapat data di database dengan valueMy tweet!
seperti yang telah kita define sebelumnya.
As simple as that.
Pendekatan 2
- Pada pendekatan 1, ada masalah yang bisa timbul jika kita hanya ngetest method
createTweet()
nya saja, yaitu ketika user belum melakukan login.
Dan juga tanpa login, fitur create tweet kita belum mencerminkan fungsi sebenarnya. - Pada step Exercise, Laravel punya
$this->actionAs($user)
untuk mensimulasikan login sebagai$user
dan$this->call()
yang bisa digunakan untuk memanggil suatu route beserta paramsnya jika ada. $response
bisa digunakan untuk ngetest http status atau redirect kemana setelah kita ngaksesroute(‘tweet.store’)
.$this->assertEquals(302, $response->status())
$this->assertRedirectedTo(route(‘tweet.create’))
Very simple
Kesimpulan
- Ada 2 pendekatan yang bisa dilakukan untuk melakukan integration test, memanggil method yang ingin ditest dan memanggil route dari method controller yang ingin kita test
- Untuk kasus create dan juga kasus yang terdapat perubahan data (update, destroy) pada method yang ingin kita test, pendekatan 2 lebih simple dan bisa mencover fitur lebih real.
- Tapi ada kasus dimana pendekatan 2 sulit dilakukan, yaitu pada next studi kasus
Studi kasus 2
Membuat fitur show tweets dari suatu user
Buat dulu fiturnya
Anggap kita melakukan test Pendekatan 2, maka:
- Harapannya kita bisa ngevalidasi variable
$tweets
yang dikirim keview(‘users.showTweets’)
pada hasil$response
- Tapi
$response
dari$this->call()
tidak bisa untuk mendapatkan hanya variable$tweets
yang dikirim ke viewusers.showTweets
.$response
hanya bisa mendapatkan body content dari request call yang dilakukan, yaitu full html padaview(‘users.showTweets’)
. Tentu sulit dilakukan validasi pada html tersebut. - Maka dari itu, perlu kita gunakan Pendekatan 1, yaitu kita panggil
$user->getTweets()
untuk ditest. Jadi test kita ubah jadi seperti ini:
Simple.
Setup database
- Saat ini baru dicoba untuk db mysql dan sqlite saja.
- Database untuk test sebaiknya disamakan dengan database yang digunakan aplikasi untuk menghindari perbedaan hasil test dengan yang di aplikasi.
- Karena Gitlab CI Javan baru support Mysql, maka kita gunakan Mysql.
4 Hal yang perlu disetting configurasinya untuk setup database
- Tambahkan config database khusus test pada
.env
DB_CONNECTION_TEST=mysql_test
DB_HOST_TEST=127.0.0.1
DB_PORT_TEST=8889
DB_DATABASE_TEST=phpunit-test
DB_USERNAME_TEST=root
DB_PASSWORD_TEST=root
2. Tambahkan connection baru pada conf/database.php
dengan menggunakan config step pertama
'mysql_test' => [
'driver' => 'mysql',
'host' => env('DB_HOST_TEST', 'localhost'),
'port' => env('DB_PORT_TEST', '3306'),
'database' => env('DB_DATABASE_TEST', 'forge'),
'username' => env('DB_USERNAME_TEST', 'forge'),
'password' => env('DB_PASSWORD_TEST', ''),
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'prefix' => '',
'strict' => true,
'engine' => null,
],
3. Gunakan koneksi database baru pada config phpunit.xml
dengan menambahkan:
<env name=”DB_CONNECTION” value=”mysql_test”/>
4. Jalankan migrate menggunakan koneksi database yang khusus mysql test.
Migrate hanya perlu dijalankan sekali di awal.
Ini untuk menghindari dilakukan migrate pada setiap test case sehingga memperlama proses test.
Untuk memastikan data tetap fresh seperti di kondisi setelah migrate pertama kali pada setiap test case, gunakan traits DatabaseTransactions
pada setiap class test.
php artisan migrate --seed --database=mysql_test
Untuk contoh-contoh lebih lanjut bisa dilihat di:
Penutup
Harapannya dengan tulisan ini, pengguna Laravel bisa lebih terbayang untuk bagaimana membuat test yang simple dan mudah dibaca namun tetap mampu melakukan test terhadap suatu fitur dengan tepat.
Tulisan ini akan terus diupdate mengikuti perkembangan knowledge dan kebutuhan berdasarkan test-test yang kami terapkan dalam project kami.
Akan ada next tulisan yang menggunakan Grails. So stay tuned!