hamonangann / hamonangann.github.io

https://hamonangann.github.io/
Creative Commons Attribution Share Alike 4.0 International
0 stars 0 forks source link

Isolasi Unit Test dengan Bantuan Test Double #4

Closed hamonangann closed 1 year ago

hamonangann commented 1 year ago

Unit test berarti tes satuan. Ini artinya unit test hanya menguji satu fungsi atau modul terhadap suatu kasus uji (test case).

Akan tetapi, bagaimana jika suatu fungsi atau modul yang dites memiliki dependensi ke fungsi, kelas, atau modul lainnya? Di sinilah peran test double diperlukan, yaitu untuk mengisolasi unit test dengan mengganti setiap dependensi dengan fungsi, kelas, atau modul tiruan yang memiliki perilaku yang dimodifikasi.

Ada beberapa level dalam test double. Kita bisa membuat sendiri test double atau menggunakan test framework.

Sebagai contoh, kita akan membuat beberapa unit test untuk menguji fungsi randomizer nama. Fungsi ini memiliki dua parameter, yaitu object randomizer yang bertugas memilih nama secara acak dan array/list nama yang hendak dirandomize.

def random_names(randomizer, names):
    if len(names) == 0:
        return "No name provided"
    return randomizer.randomize(names)

Dummy

Dummy adalah test double paling sederhana. Ia tidak punya fungsionalitas apa-apa dan biasanya digunakan hanya untuk menempati parameter sebuah fungsi.

Sebagai contoh, kita dapat membuat test case untuk pemanggilan fungsi dengan array kosong. Dengan demikian, tentu saja kita tidak akan memanggil fungsi randomize. Jadi kita bisa gunakan dummy function sebagai berikut

from random_names import random_names

import unittest

class DummyRandomizer:
    pass

class TestFaktorial(unittest.TestCase):
    def test_empty_name(self):
        result = random_names(DummyRandomizer(), [])
        self.assertEqual(result, "No name provided")

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

Stub

Stub adalah dummy yang naik level. Fungsi pada Dummy tidak bisa digunakan sama sekali, karena tidak memiliki efek atau return value. Di sisi lain, stub bisa mengembalikan return value tertentu yang biasanya disesuaikan dengan kebutuhan tes. Sebagai contoh, stub bisa digunakan untuk selalu mengembalikan string bernama "Budi"

import unittest

class DummyRandomizer:
    pass

class StubRandomizer:
    def randomize(self, names):
        return "Budi"

class TestFaktorial(unittest.TestCase):
    def test_empty_name(self):
        result = random_names(DummyRandomizer(), [])
        self.assertEqual(result, "No name provided")

    def test_single_name(self):
        result = random_names(StubRandomizer(), ["Budi"])
        self.assertEqual(result, "Budi")

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

Spy

Spy adalah level berikutnya dari stub. Spy punya fungsionalitas tambahan sesuai kebutuhan dalam unit test, yaitu mencatat berapa kali fungsinya dipanggil atau dipanggil dengan parameter apa. Kita akan membuat contoh spy sederhana untuk test_single_name yang telah kita buat.

import unittest

class SpyRandomizer:
    def __init__(self):
        self.randomize_method_called = False

    def randomize(self, names):
        self.randomize_method_called = True
        return "Budi"

class TestFaktorial(unittest.TestCase):
    def test_single_name(self):
        randomizer = SpyRandomizer()
        result = random_names(randomizer, ["Budi"])
        self.assertEqual(result, "Budi")
        self.assertEqual(randomizer.randomize_method_called, True)

Mocks

Mocks adalah spy dalam versi yang lebih canggih. Jika spy punya kemampuan untuk mendengar, mock punya kemampuan untuk merangkum apa yang ia dengar. Misalnya dalam suatu test, fungsi yang ditest harus memanggil fungsi lain A() 1 kali, B() 1 kali, dan C() 2 kali. Jika menggunakan spy kita bisa membutuhkan 3-4 kali assertion untuk memastikan setiap fungsi dipanggil dengan jumlah yang tepat. Akan tetapi, mocks dilengkapi dengan fungsionalitas verify sehingga kita cukup melakukan assertion sebanyak 1 kali.

Kebanyakan test framework membuat objek/fungsi dalam bentuk mocks ini.

import unittest

class MockRandomizer:
    def __init__(self):
        self.count_called = 0
        self.expected_methods_called = {}

    def expect(self, expectations):
        for ex in expectations:
            self.expected_methods_called[ex] = False

    def verify(self):
        for val in self.expected_methods_called.values():
            if val != True:
                return False
        return True

    def randomize(self, names):
        self.count_called += 1
        self.expected_methods_called["randomize"] = True
        return "Budi"

class TestFaktorial(unittest.TestCase):
    def test_single_name(self):
        randomizer = MockRandomizer()
        randomizer.expect(["randomize"])
        result = random_names(randomizer, ["Budi"])
        self.assertEqual(result, "Budi")
        self.assertEqual(randomizer.verify(), True)

Fakes

Fakes punya fungsionalitas yang serupa dengan fungsi asli, tapi dalam behavior yang palsu. Misalnya pada randomizer user di atas, fakes dapat dibuat untuk selalu mengembalikan user pertama dalam list, atau null jika listnya kosong. Tentu saja kita tidak mau production app kita selalu mengembalikan user pertama, tapi untuk kebutuhan testing dan development ini akan sangat berguna untuk menguji suatu behavior.

import unittest

class FakeRandomizer:
    def __init__(self):
        self.count_called = 0
        self.expected_methods_called = {}

    def expect(self, expectations):
        for ex in expectations:
            self.expected_methods_called[ex] = False

    def verify(self):
        for val in self.expected_methods_called.values():
            if val != True:
                return False
        return True

    def randomize(self, names):
        self.count_called += 1
        self.expected_methods_called["randomize"] = True

        if len(names) == 0:
            return ""
        else:
            return names[0]

class TestFaktorial(unittest.TestCase):
    def test_multiple_names(self):
        randomizer = FakeRandomizer()
        randomizer.expect(["randomize"])
        result = random_names(randomizer, ["Cheryl", "Ibnu", "Matthew"])
        self.assertEqual(result, "Cheryl")
        self.assertEqual(randomizer.verify(), True)

Referensi

Eftimov, I. (2019). Testing in Go: Test Doubles by Example. https://ieftimov.com/posts/testing-in-go-test-doubles-by-example/

Test Double at XUnitPatterns.com. (n.d.). http://xunitpatterns.com/Test%20Double.html

Source code program di artikel ini dapat diakses pada link ini

hamonangann commented 1 year ago

tambahan: beberapa test framework menyebut stub sebagai mock. seringkali ini yang membuat kebingungan