Antara Saya, Test, dan Mock
Disclaimer: Tulisan ini bukan tulisan tutorial, ini refleksi bagaimana saya salah mengerti kebutuhan mock dalam testing.
Satu hal dalam testing yang dulu bikin saya mengganjal adalah mock, itu semua terjadi karena saya salah mengerti maksud dari mock itu sendiri, saya terlalu terfokus kepada mock response, saya berpikir saat itu "apa bedanya hardoced response dengan mock?". Selain tentang response yang sering disinggung saat belajar dan bertanya biasanya lebih berfokus kepada "agar bisa ngetes third party tanpa memanggil yang sebenarnya". Dua hal itu jadi membuat saya bingung.
Setelah di kantor mewajibkan test dan juga sering mengunakan mock, yang awalnya ikut-ikutan sekarang mulai sedikit ngerti dan menjawab kebingungan.
Third Party Call
Salah satu yang sering dibahas adalah tadi, dengan mock jika dalam satu fungsi kita memanggil third party maka ada kemungkinan test kita jadi lama dan juga tergantung si third party-nya. Contoh paling sederhana adala fitur forgot password.
Semua contoh di sini menggunakan python, tapi secara konsep seharusnya di bahasa apapun sama saja.
#main.py
from third_party import send_otp
from common import Result
userPhone = {"john": "081234567", "doe": "0897654422"}
def forgot_password(username: str) -> Result:
# Change to db or other function to get user
if username not in userPhone:
return {"success": False, "message": "User not found"}
phoneNumber = userPhone[username]
res = send_otp(phoneNumber)
return res
lalu ini fungsi third party-nya untuk mengirim otp
# third_party.py
from common import Result
from random import randint
from time import sleep
def send_otp(number: str) -> Result:
# Simulate intermitten long running process
delay = randint(0, 5)
sleep(delay)
return {"success": True, "message": ""}
Lalu ini test yang saya buat.
#test.py
import unittest
from main import forgot_password
class TestForgotPassword(unittest.TestCase):
def test_forgot_password_success(self):
# input
username = "john"
# expected
expected = {"success": True, "message": ""}
result = forgot_password(username)
self.assertEqual(result, expected)
Saat melakukan test hasilnya akan seperti ini
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 4.001s
OK
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.000s
OK
Test nya jadi susah, kadang lama kadang sebentar.
Karena seperti yang dibilang tadi agar testing ini tidak terpengaruh maka si third party ini perlu dimock.
import unittest
from main import forgot_password
from unittest.mock import patch
class TestForgotPassword(unittest.TestCase):
@patch("main.send_otp")
def test_forgot_password_success(self, mock):
# input
username = "john"
# expected
expected = {"success": True, "message": ""}
expectedMock = {"success": True, "message": ""}
mock.return_value = expectedMock
result = forgot_password(username)
self.assertEqual(result, expected)
Hasilnya
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Flow Function
Pada contoh di atas memang menjawab menghilangkan ketergantungan dari third party, tapi dulu saya merasa kalau hanya mengubah response yang diharapkan tidak otomatis jadi benar kan, maksud dari tidak otomatis benar adalah jika ada yang mengubah kode forgot_password jadi seperti di bawah ini
def forgot_password(username: str) -> Result:
# Change to db or other function to get user
# Default response
res = {"success": True, "message": ""}
if username not in userPhone:
return {"success": False, "message": "User not found"}
phoneNumber = userPhone[username]
return res
Saat kita jalankan testnya masih akan sukses
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Kenapa sukses? Padahal fungsi send_otp yang penting hilang? Jadi yang lupa password tidak akan dikirimkan otp? Salah dong testnya? Di sini yang saya maksud bahwa jika yang dibicarakan adalah hanya "mengubah respon" dan "tidak tergantung dengan third party" itu membuat pertanyaan "jadi mock buat apa?"
Sehingga balik lagi, yang harus dites kan bukan si "mock" nya tapi fitur itu sendiri:
- Forgot password kalau sukses wajib memanggil send_otp.
- Forgot password kalau tidak menemukan user jadi false, dan tidak manggil send_otp
- Karena yang diinput adalah username sedangkan send_otp itu phone number, apakah benar kita mengirimkan phone number ke send_otp?
Sehingga kalau sudah fokus ke fungsi utamanya sendiri maka ada perubahan di testingnya
import unittest
from main import forgot_password
from unittest.mock import patch
class TestForgotPassword(unittest.TestCase):
@patch("main.send_otp")
def test_forgot_password_success(self, mock):
# input
username = "john"
# expected
expected = {"success": True, "message": ""}
expectedMock = {"success": True, "message": ""}
phoneNumber = "081234567"
# mock the response
mock.return_value = expectedMock
result = forgot_password(username)
totalCall = mock.call_count
mock.assert_called_with(phoneNumber)
self.assertEqual(result, expected)
self.assertEqual(1, totalCall)
Sehingga ketika melakukan test ulang mendapatkan pesan
AssertionError: expected call not found.
Expected: send_otp('081234567')
Actual: not called.
Lalu kembalikan fungsi forgot_password seperti semula
def forgot_password(username: str) -> Result:
# Change to db or other function to get user
res = {"success": True, "message": ""}
if username not in userPhone:
return {"success": False, "message": "User not found"}
phoneNumber = userPhone[username]
res = send_otp(phoneNumber)
return res
Jalankan testnya lagi
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
➜ mock python -m unittest test.TestForgotPassword
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Perubahan tesnya kan hanya sedikit tapi cukup membuat tenang.
mock.assert_called_with(phoneNumber)
: Memastikan bahwa fungsi nya menerima inputan sesuai yang diharapkan
totalCall = mock.call_count
dan self.assertEqual(1, totalCall)
: Memastikan bahwa fungsi send_otp hanya dipanggil sekali.
Dengan tambahan ini saat buat skenario gagal bisa seperti berikut
import unittest
from main import forgot_password
from unittest.mock import patch
class TestForgotPassword(unittest.TestCase):
@patch("main.send_otp")
def test_forgot_password_success(self, mock):
# input
username = "john"
phoneNumber = "081234567"
# expected
expected = {"success": True, "message": ""}
expectedMock = {"success": True, "message": ""}
# mock the response
mock.return_value = expectedMock
result = forgot_password(username)
totalCall = mock.call_count
mock.assert_called_with(phoneNumber)
self.assertEqual(result, expected)
self.assertEqual(1, totalCall)
@patch("main.send_otp")
def test_forgot_password_false(self, mock):
# input
username = "johnx"
# expected
expected = {"success": False, "message": "User not found"}
expectedMock = {"success": True, "message": ""}
# mock the response
mock.return_value = expectedMock
result = forgot_password(username)
totalCall = mock.call_count
self.assertEqual(result, expected)
self.assertEqual(0, totalCall)
Di contoh yang gagal ada bagian ini self.assertEqual(0, totalCall)
ini membantu saya yakin bahwa jika terjadi kesalahan saat tidak menemukan user maka tidak akan manggil send_otp.
➜ mock python -m unittest test.TestForgotPassword
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK
Jadi kesalahan saya dalam belajar mock saat itu adalah:
- Terlalu fokus kepada objek yang di-mock bukan kepada fungsi yang akan di-test.
- Fokus mock justru bukan sekadar di response tapi apakah fungsinya terpanggil dan apakah parameter yang dikirim itu sesuai atau tidak