Mocking untuk Menangani Error di Testing Django

Mocking untuk Menangani Error di Testing Django
Photo by Brando Makes Branding / Unsplash

Saya bukan penggemar mock dalam testing sebenarnya, apalagi jika yang dites adalah fungsionalitas basis data. Iya, kalau saya buat CRUD dengan ORM, saya tes dengan memastikan datanya masuk ke dalam basis data, datanya yang disimpan benar, sehingga saya menghindari mock untuk kondisi seperti ini. Karena menurut saya tidak peduli fungsi yang kamu buat, ketika data yang kamu gunakan ngaco hasilnya ngaco.

Tapi ya bukan berarti saya tidak menggunakan mock sama sekali. Untuk hal-hal seperti third party package, third party API, saya cenderung menggunakan mock, tapi ada satu lagi hal yang saya gunakan untuk mock yaitu perkara galat basis data.

Contoh di bawah ini saat saya menggunakan Django dan Django ORM. Ketika saya menggunakan Django ORM saya jarang membuat testing yang CRUD tadi, saya percaya lah ama Django, jadi biasanya yang saya test justru negatif test-nya.

Contoh kasus

Contoh jika kita memiliki sebuah fungsi yang kita sadar akan terjadi galat dalam rentang waktu tertentu sehingga kita memastikan dengan cara jika terjadi galat kita akan coba beberapa kali.

# app/service.py
def insert_retry(data) -> int:
    count = 1
    while count <= 5:
        try:
            Play.objects.create(**data)
            break
        except Exception as e:
            print(e)
            count += 1
    return count

Dalam kode di atas, jika terjadi kegagalan akan dicoba sampai 5 kali sebelum dinyatakan gagal sepenuhnya. Untuk mengetes hal di atas kita bisa gunakan mock side effect jika konteksnya Django ORM:

class InsertRetryTest(TestCase):

    @patch('app.service.Play.objects.create')
    def test_retry_always_fail(self, mock_create):

        before = Play.objects.count()
        mock_create.side_effect = Exception("Always Error")

        data = {
            "title": "test",
            "description": "desc"
        }
        result = insert_retry(data)

        after = Play.objects.count()

        # count start from 1 so will stop at 6
        self.assertEqual(result, 6)
        # make sure we call the ORM 5 times
        self.assertEqual(mock_create.call_count, 5)
        # before and after equal, no insert
        self.assertEqual(after, before)

Jalankan tes

./manage.py test

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 tests in 0.001s

OK

Kita juga bisa ubah kode di atas, misal ada jenis galat yang sebenarnya gak perlu diulang karena percuma hasilnya selalu salah, jadi lebih baik langsung diselesaikan saja.

def insert_retry(data) -> int:
    count = 1
    while count <= 5:
        try:
            Play.objects.create(**data)
            break
        except (ValueError, DatabaseError):
            # no need to retry
            return count
        except Exception as e:
            count += 1
    return count

Kita tambahkan tes untuk skenario tersebut

class InsertRetryTest(TestCase):

    @patch('app.service.Play.objects.create')
    def test_retry_always_fail(self, mock_create):

        before = Play.objects.count()
        mock_create.side_effect = Exception("Always Error")

        data = {
            "title": "test",
            "description": "desc"
        }
        result = insert_retry(data)

        after = Play.objects.count()

        # count start from 1 so will stop at 6
        self.assertEqual(result, 6)
        # make sure we call the ORM 5 times
        self.assertEqual(mock_create.call_count, 5)
        # before and after equal, no insert
        self.assertEqual(after, before)

    @patch('app.service.Play.objects.create')
    def test_no_retry_fail(self, mock_create):
        before = Play.objects.count()
        mock_create.side_effect = [ValueError, DatabaseError]

        data = {
            "title": "test",
            "description": "desc"
        }
        result = insert_retry(data)

        after = Play.objects.count()

        # count start from 1 so will stop at 1
        self.assertEqual(result, 1)
        # make sure we call the ORM 1 time
        self.assertEqual(mock_create.call_count, 1)
        # before and after equal, no insert
        self.assertEqual(after, before)

Jalankan tes nya lagi:

Creating test database for alias 'default'...
System check identified no issues (0 silenced).
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Read more