Rabu, 21 September 2016

Belajar Unit Testing tahap demi tahap

unittest

Unit adalah bagian terkecil yang bisa dites dari sebuah program. Fungsi dari unit testing adalah melakukan pengujian setiap bagian terkecil dari sebuah program secara independen, terpisah dari bagian lain dari program tersebut. Dengan dilakukannya unit test, maka akan dapat dilihat bagian mana dari program yang bermasalah dan bagian mana yang berjalan baik.

Sebuah unit bisa berupa sebuah modul, tapi bisa juga sebuah function. Sebagai contoh, untuk program kalkulator, maka contoh unit terkecil yang bisa dites adalah:

  • penambahan
  • pengurangan
  • perkalian
  • pembagian
  • dan fungsi-fungsi lain

Unit test mempunyai beberapa kelebihan, yaitu:

  • Memberikan perasaan puas pada programmer bahwa dia sudah sampai ke milestone tertentu dengan menyelesaikan modul tertentu
  • Memberikan kepercayaan diri pada programmer bahwa kode yang dibuat benar dan berjalan dengan baik. Setiap unit test akan memberikan sinyal PASS atau FAILED ketika dijalankan. Jika programmer membuat atau mengubah kodenya, dan ternyata hasil unit testnya PASS, maka programmer bisa yakin bahwa program yang dibuatnya benar
  • Salah satu masalah besar dalam proyek perangkat lunak adalah ketika integrasi modul. Ketika tahap integrasi, modul yang tadinya berjalan dengan baik, tiba-tiba bisa berubah menjadi bermasalah. Unit tes memastikan bahwa masing-masing unit melakukan tugasnya masing-masing. Dengan demikian, ketika diintegrasikan, pengujian perangkat lunak hasil integrasi menjadi lebih mudah karena setiap bagian terkecil dari aplikasi itu sudah diuji.
  • Memudahkan proses regression testing. Dalam proses pengembangan perangkat lunak, bugs yang pernah muncul dan sudah di-resolve seringkali muncul lagi ketika bagian lain dalam perangkat lunak tersebut diubah. Di situlah peran regression testing menjadi penting. Regression testing adalah proses pengujian terhadap perubahan yang dilakukan untuk memastikan bahwa perubahan itu tidak mengganggu fungsionalitas yang diharapkan. Ketika ditemukan bugs, kemudian bugs tersebut dituliskan dalam unit test, maka proses untuk regression testing akan menjadi jauh lebih mudah dan cepat.

Di sisi lain, terdapat juga kekurangan unit testing, yaitu:

  • Manual testing tidak akan bisa digantikan sepenuhnya dengan testing otomatis. Hal ini karena ada terlalu banyak kemungkinan yang terjadi dalam dunia nyata yang mungkin tidak terpikirkan oleh programmer ketika membuat unit test.
  • Unit test tidak bisa dijalankan dalam lingkungan yang nyata. Sebagai contoh, dalam pengembangan perangkat mobile, unittest hanya bisa dijalankan dalam compiler tanpa bisa dijalankan di device sesungguhnya.
  • Meskipun sebuah program telah lolos semua unit test, hal itu tidak menunjukkan bahwa program itu bugs-free. Seperti dalam poin sebelumnya, ada banyak hal yang tidak bisa dicover oleh unittest dalam dunia nyata.

Unit Test di Python

Unit test di python dulu dikenal dengan nama PyUnit sebagai credit untuk xUnit, sebuah framework yang ditulis oleh Kent Beck pada tahun 1998. xUnit diadopsi dalam berbagai bahasa pemrograman. Oleh karena itu, jika kita paham unit test dalam satu bahasa pemrograman, maka kita akan dengan mudah membawanya dalam bahasa pemrograman yang lain.

Beberapa ketentuan dalam unit test adalah sebagai berikut:

  • Di bagian atas harus dinyatakan import unittest
  • Class yang didefinisikan harus menginduk pada unittest.TestCase
  • Semua fungsi yang akan melakukan pengujian harus didahului dengan nama test_

Mencicipi Unittest

Kita akan membuat contoh sederhana dari unittest. Dalam hal ini, unit test tidak akan menguji apapun.

import unittest

class Coba(unittest.TestCase):
    def test_satu(self):
        self.assertEqual(3,4)

    def test_dua(self):
        self.assertEqual(3*4, 12)

if __name__ == '__main__':
    unittest.main()

Misal file tersebut disimpan dengan nama coba1.py, maka kita bisa menjalankannya dengan menggunakan salah satu dari perintah berikut:

  • python coba1.py
  • python -m unittest -v coba1.py
  • python -m unittest
  • Opsi lain untuk menjalankan unittest bisa dilihat di sumber yang lain

Lebih lanjut tentang perintah-perintah tersebut akan dijelaskan dalam diskusi berikut.


$ python coba1.py

Perintah ini akan menghasilkan output sebagai berikut:

.F
======================================================================
FAIL: test_satu (__main__.Coba)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "cobates.py", line 5, in test_satu
    self.assertEqual(3,4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.001s

Dari output tersebut, dapat dilihat bahwa perintah ini menampilkan hasil dengan format sebagai berikut:

  • Tes yang berhasil ditulis dengan tanda titik (.), sedangkan yang gagal (FAILED) ditulis dengan tanda F
  • Hanya ditampilkan tes yang gagal (FAILED) saja

$ python -m unittest -v coba1.py

Perintah ini akan menjalankan seperti perintah sebelumnya, tapi dengan opsi verbosity. Opsi verbosity akan membuat semua tes ditampilkan hasilnya. Pada output berikut, perhatikan bahwa test_dua ditampilkan bahwa hasilnya OK. Bandingkan dengan output sebelumnya. Outputnya adalah sebagai berikut:

test_dua (cobates.Coba) ... ok
test_satu (cobates.Coba) ... FAIL

======================================================================
FAIL: test_satu (cobates.Coba)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/MyWorks/Scram/mengajar/software-testing/cobates.py", line 5, in test_satu
    self.assertEqual(3,4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.002s

FAILED (failures=1)

Opsi verbosity juga akan membuat output menampilkan kata-kata yang kita tampung dalam perintah print. Misal kita ubah file coba1.py kita sebagai berikut:

import unittest

class Coba(unittest.TestCase):

    def test_satu(self):
        print("ini dari test satu")
        self.assertEqual(3, 4)

    def test_dua(self):
        self.assertEqual(3*4, 12)

if __name__ == '__main__':
    unittest.main()

Perhatikan bahwa dalam test_satu kita tambahkan satu baris untuk menampilkan "ini dari test satu". Output yang didapatkan ketika kita menjalankan perintah python -m unittest -v coba1.py adalah sebagai berikut:

test_dua (coba1.Coba) ... ok
test_satu (coba1.Coba) ... ini dari test satu
FAIL

======================================================================
FAIL: test_satu (coba1.Coba)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/MyWorks/Scram/mengajar/software-testing/code/coba1.py", line 7, in test_satu
    self.assertEqual(3, 4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

$ python -m unittest

Perhatikan bahwa dalam perintah ini, kita tidak mendefinisikan nama file yang akan dieksekusi. Perintah ini akan melakukan self discovery yaitu menjalankan tes-tes yang ada di folder tersebut. Sebuah tes akan dieksekusi apabila dalam nama filenya sesuai dengan template. Secara default, template dari nama file yang akan dieksekusi oleh self discovery adalah yang diawali dengan test*.py. Apabila kita tidak mengganti nama file kita yaitu coba1.py, maka ketika perintah ini dieksekusi, maka keluarannya adalah sebagai berikut:

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Jika kita mengganti nama file kita dengan testcoba.py, maka keluaran dari perintah python -m unittest adalah sebagai berikut:

.ini dari test satu
F
======================================================================
FAIL: test_satu (testcoba.Coba)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/MyWorks/Scram/mengajar/software-testing/code/testcoba.py", line 7, in test_satu
    self.assertEqual(3, 4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.001s

FAILED (failures=1)

Kita bisa mengganti template dari fitur self discovery. Sebagai contoh, ketika file yang kita eksekusi berawalan coba, maka kita bisa melakukan self discovery terhadap tes tersebut dengan memberikan sebagai berikut

python -m unittest discover -p "coba*.py"

Apabila dalam folder tersebut (ataupun folder-folder di bawahnya) terdapat file dengan nama yang diawali dengan kata-kata coba, maka tes dalam file tersebut akan dieksekusi.


seTup dan tearDown

setUp merupakan fungsi dalam unittest yang akan selalu dieksekusi sebelum menjalankan masing-masing test. Sedangkan tearDown adalah fungsi dalam unittest yang akan selalu dieksekusi setelah menjalankan masing-masing test.

Untuk mempelajari lebih lanjut tentang fungsi setUp dan tearDown ini, berikut akan dibuat sebuah contoh:

import unittest

class Coba(unittest.TestCase):
    def setUp(self):
        print("ini dari setup")

    def test_a(self):
        print("ini dari test_a")

    def test_b(self):
        print("ini dari test_b")

    def tearDown(self):
        print("ini dari teardown")

if __name__ == '__main__':
    unittest.main()

Simpan program tersebut dengan nama coba2.py dan eksekusi dengan menjalankan perintah sebagai berikut:

$ python -m unittest -v coba2.py

Perintah tersebut akan menghasilkan output sebagai berikut:

test_a (coba2.Coba) ... ini dari setup
ini dari test_a
ini dari teardown
ok
test_b (coba2.Coba) ... ini dari setup
ini dari test_b
ini dari teardown
ok

----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Dari contoh output di atas, anda dapat melihat bahwa setUp selalu dipanggil sebelum perintah dalam setiap tes dieksekusi, sedangkan tearDown selalu dipanggil setelah setiap tes selesai dieksekusi. Dengan demikian, setUp akan selalu dieksekusi meskipun sebuah test akan FAILED. Tapi, apakah tearDown akan dieksekusi juga?

Untuk menjawab pertanyaan itu, kita akan membuat file selanjutnya yaitu coba3.py

import unittest

class Coba(unittest.TestCase):
    def setUp(self):
        print("ini dari setup")

    def test_a(self):
        print("ini dari test_a")

    def test_failed(self):
        self.assertEqual(3, 4)

    def tearDown(self):
        print("ini dari teardown")


if __name__ == '__main__':
    unittest.main()

Ketika kita jalankan, maka outputnya adalah sebagai berikut:

test_a (coba3.Coba) ... ini dari setup
ini dari test_a
ini dari teardown
ok
test_failed (coba3.Coba) ... ini dari setup
ini dari teardown
FAIL

======================================================================
FAIL: test_failed (coba3.Coba)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Volumes/MyWorks/Scram/mengajar/software-testing/code/coba3.py", line 11, in test_failed
    self.assertEqual(3, 4)
AssertionError: 3 != 4

----------------------------------------------------------------------
Ran 2 tests in 0.003s

FAILED (failures=1)

Dari output tersebut, maka dapat kita simpulkan bahwa fungsi tearDown akan selalu dieksekusi, meskipun test tersebut FAILED.

Mengorganisir tes

Dalam contoh sebelumnya, kita dapat melihat bahwa unit test tidak selalu mengeksekusi setiap tes secara urut (lihat hasil eksekusi dari python -m unittest -v coba1.py). Untuk membuat tes dilakukan dengan urutan tertentu, maka kita perlu mengorganisirnya dengan menggunakan suite.

Untuk bagian ini, kita akan membuat file coba4.py sebagai berikut:

import unittest

class Coba(unittest.TestCase):
    def test_a(self):
        print("ini dari test a")

    def test_b(self):
        print("ini dari test b")

    def test_c(self):
        print ("ini dari test c")

def suite():
    suite = unittest.TestSuite()
    suite.addTest(Coba('test_c'))
    suite.addTest(Coba('test_a'))
    return suite

if __name__ == '__main__':
    runner = unittest.TextTestRunner()
    test_suite = suite()
    runner.run(test_suite)

Perhatikan bahwa untuk test suite, syntaxnya agak berbeda dengan syntax sebelumnya. Untuk test suite ini, kita perlu mendeklarasikan fungsi suite yang digunakan untuk memasukkan tes apa saja dalam suite tersebut.

def suite():
    suite = unittest.TestSuite()
    suite.addTest(Coba('test_c'))
    suite.addTest(Coba('test_a'))
    return suite

Perhatikan bahwa meskipun dalam kelas Coba kita mempunyai tiga tes, yaitu test_a, test_b dan test_c, di sini kita hanya menambahkan test_c dan test_a saja. Perhatikan juga bahwa dalam suite tersebut, test_c diletakkan di atas, sehinga akan dieksekusi sebelum tets_a.

Selain itu, perhatikan juga bahwa di dalam main execution, kita tidak mendefinisikan unittest.main(), tapi kita definisikan sebagai berikut:

runner = unittest.TextTestRunner()
test_suite = suite()
runner.run(test_suite)

Kita menggunakan class TextTestRunner yang ada di dalam unittest.

Untuk menjalankannya, kita tidak menggunakan perintah python -m unittest -v coba4.py, tapi dengan menggunakan python coba4.py.

Apabila kita menggunakan perintah python -m unittest -v coba4.py, maka outputnya adalah sebagai berikut:

test_a (coba4.Coba) ... ini dari test a
ok
test_b (coba4.Coba) ... ini dari test b
ok
test_c (coba4.Coba) ... ini dari test c
ok

----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Perhatikan bahwa perintah tersebut tidak mengeksekusi tes yang didefinisikan dalam test suite. Perhatikan bahwa semua tes dilakukan, dan urutannya tidak sesuai yang kita definisikan dalam test suite.

Untuk mengeksekusi tes yang didefinisikan dalam test suite, maka kita jalankan dengan menggunakan perintah python coba4.py. Keluaran dari pentintah itu adalah sebagai berikut:

ini dari test c
.ini dari test a
.
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Perhatikan bahwa hanya dua tes yang dijalankan, dengan urutan test_c dieksekusi terlebih dahulu sebelum test_a.


Skipping test

Dalam kondisi tertentu (misal karena belum diimplementasi atau ketika ingin fokus pada beberapa tes saja), kita ingin men-skip tes-tes tertentu. Hal ini bisa dilakukan dengan memberi anotasi pada tes yang dimaksud.

Untuk keperluan ini, kita akan membuat file coba5.py sebagai berikut:

import unittest

class Coba(unittest.TestCase):
    def test_a(self):
        print("ini adalah tes a")

    @unittest.skip("ini diskip aaaah")
    def test_b(self):
        print("ini adalah tes b yang mau kita skip")

    def test_c(self):
        print("ini adalah tes c")

    @unittest.skip("tesnya diskip dulu ya")
    def test_d(self):
        print("ini adalah tes d yang mau kita skip")

    def test_e(self):
        print("ini adalah tes e")


if __name__ == "__main__":
    unittest.main()

Perhatikan bahwa di atas test_b dan test_d kita memberikan anotasi @unittest.skip. Perintah ini memberitahu kompiler bahwa tes yang ditandai dengan anotasi tersebut akan di-skip.

Apabila coba5.py dieksekusi dengan menggunakan perintah python coba5.py, maka keluarannya adalah sebagai berikut:

ini adalah tes a
.sini adalah tes c
.sini adalah tes e
.
----------------------------------------------------------------------
Ran 5 tests in 0.000s

OK (skipped=2)

Sedangkan apabila dieksekusi dengan menggunakan perintah python -m unittest -v coba5.py, hasilnya adalah sebagai berikut:

test_a (coba5.Coba) ... ini adalah tes a
ok
test_b (coba5.Coba) ... skipped 'ini diskip aaaah'
test_c (coba5.Coba) ... ini adalah tes c
ok
test_d (coba5.Coba) ... skipped 'tesnya diskip dulu ya'
test_e (coba5.Coba) ... ini adalah tes e
ok

----------------------------------------------------------------------
Ran 5 tests in 0.001s

OK (skipped=2)

Perhatikan bahwa yang dieksekusi hanyalah test_a, test_c dan test_c. Perhatikan bahwa test_b dan test_d di-skip, dan jumlah test yang di-skip ini dicantumkan di bawah (skipped=2).