Tutorial RESTful API dengan FastAPI Python + MongoDB Part 2— Unit Test

Menulis Unit Test pada FastAPI Python, Mocking Databasenya biar gampang!

Hudya
8 min readOct 19, 2020

Halo semua, Kiddy disini dan pada kesempatan kali ini saya ingin berbagi insight mengenai cara membuat RESTful API dengan FastAPI Python menggunakan Database MongoDB dan kita akan melengkapinya dengan pendekatan unit test.

Tutorial ini adalah lanjutan dari tutorial sebelumnya, yaitu https://kiddyxyz.medium.com/tutorial-restful-api-dengan-fastapi-python-mongodb-dan-test-driven-development-part-1-instalasi-d085c7205a4f, jika kamu belum mencobanya maka harap kesana terlebih dahulu yah~

Oke semua, di part 2 ini kita akan melanjutkan tutorial selanjutnya untuk melengkapi projek dengan unit test

Untuk yang ngikutin tutorial saya pada tanggal 18 Oktober 2020 saat pertama kali artikel part 1 dibuat. mohon copy paste kembali code di bagian UserController dan Todo Controller ya karena ada sedikit revisi code yang saya baru engeh ada bugs disana (hehehe), dan bagi yang melakukan clone dari repository saya, mohon pull kembali dari master yah~

Oke kita lanjuts!

Sekarang kita masuk ke bagian folder test, dan buat satu file test bernama test_user.py:

Copy paste isi codenya:

from unittest import TestCase

import json
from bson import ObjectId

from mongoengine import connect, disconnect
from starlette.testclient import TestClient

from app import app
from app.models.user import Users

"""
Inisialisasi Test Client sehingga menjalankan FastAPI tanpa perlu dijalankan dengan uvicorn.
"""
client = TestClient(app)


class TestUser(TestCase):

@classmethod
def setUpClass(cls):
"""
setUp function akan dijalankan saat file test pertama kali djialankan.
"""
disconnect()
connect('mongoenginetest', host='mongomock://localhost/mocking_db')

@classmethod
def tearDownClass(cls):
"""
tearDown function akan dijalankan saat seluruh test selesai dijalankan.
"""
disconnect()

Oke saya jelasin ya, jadi kita akan memanggil TestClient, yaitu sebuah client untuk membuat test unit sehingga otomatis projek FastAPI kita dijalankan tanpa perlu menjalankannya dengan Uvicorn namun dalam keadaan testing, sehingga kamu ngga akan bisa akses dan cuma si test unit yang akses.

Nah kemudian kita buat fungsi setUp, dimana setUp function akan dijalankan saat file test pertama kali djialankan. Jadi sebelum test dijalanin dia akan ngapain dulu gitu, misalnya ngopi, curhat, atau bahkan gossipin tetangga (lho). Disini kita menyuruh client untuk disconnect, alias kita nyuruh mereka matiin koneksi ke DB, soalnya saat pertama kali API kita kan jalan, kita merintahin API untuk konek ke MongoDB, nah kita bilang untuk disconnect itu.

Tujuannya adalah agar kita bisa konek ke mongomocking, yaitu sebuah library yang membuat kita seolah-olah koneksi ke database, padahal nggak! Hahahaha. Namanya aja Mocking alias pura-pura atau ngeledek, jadi itu keliatan kaya kejadian, padahal ngga dan mereka jalannya di session (cmiiw). So, kalo kamu coba cek ke mocking_db di mongodb kamu, kamu ngga akan pernah nemu data apapun, ya iyalah kan di mocking! Nah karena di tutorial part 1 kita udah install mongomock makanya kita bisa mocking si DB.

Nah di tearDown function ini adalah fungsi yang akan dieksekusi isinya ketika semua file test kita djialankan. Jadi dia akan disconnect ke db si mocking itu. Tujuannya ya biar terputus sesinya, meskipun cuma mocking (cmiiw).

Oke lanjut, sekarang kita akan mulai masukkin test-test dari possibility yang terjadi, setiap code yang saya tulis dibawah langsung dicopy paste atau ditulis ulang aja (terserah), abis itu dijalanin.

def test_insert_user(self):
name = "Hudya"

response = client.post("/users", json={"name": name})
assert response.status_code == 200

user = Users.objects(name=name).first()
assert user.name == name

Di fungsi ini kita ngecreate user dengan keadaan normal, sama seperti ketika kamu buat di postman gitu deh.

Nah sekarang coba jalanin codenya, dengan cara.

(venv) kiddy@ubuntu:~/code/python/learnfastapi$ pytest

Cukup eksekusi pytest dan dia otomatis nyari yang ada kata test_xxx.py-nya di folder tests.

Hasil pytest

mantap josss gile lah, satu test berhasil kita jalankan, sekarang coba aja cek sendiri ke mongodb kamu, database mocking_db dan table users. Kamu ngga akan nemuin data yang baru aja kamu input.

Oke sekarang lanjut ke fungsi kedua:

def test_insert_user_with_long_name(self):
name = "SngQ8CL1kqtowD4rl1kYrWG2WmLhvB6HQ7exaY3a5fFkG6LPBn4s" \
"Gtc5HZw7QHiQtsLKrAX7G7wBPp5of6utYDeTLllNPMJfE1m"

response = client.post("/users", json={"name": name})
assert response.status_code == 200

user = Users.objects(name=name).first()
assert user.name == name

Fungsi kedua, kita coba insert dengan 99 karakter, ya seharusnya sih ngga boleh ya kalo secara logic, cuma karena saya ngga buat validasi nama > 50 karakter yaudah lah hehehe, kalo agan-agan mau silahkan nanti dibuat konsep seperti ini ya. Kenapa gitu? Soalnya kalo QA Engineer itu harus ngetes fungsional even dengan hal yang ngga masuk akal, misalnya masukkin 10000 karakter ke JSON.

“Ya emang siapa yang kepikiran bang masukkin 10000 karakter?”

Mungkin bukan kamu, tapi ada aja orang jahil, tugas kita sebagai engineer ya ngebuat hal-hal yang “bisa aja” jadi bener-bener ngga bisa.

assert user.name == name adalah untuk memastikan di database bener-bener ada nama itu di database kita (meskipun kita mocking).

def test_insert_user_with_empty(self):
name = ""

response = client.post("/users", json={"name": name})
assert response.status_code == 400

Test ini untuk ngetes user yang namanya kosong, otomatis kodenya 400 karena ngga boleh dong ngasih nama kosong.

def test_insert_user_without_name_parameter(self):
response = client.post("/users", json={})
assert response.status_code == 400

Nah sekarang gimana apa jadinya kalo kita kirim JSON kosong? Harus error! Jadi kamu udah tau itu error, dan tugas kamu kasitau si program kalo itu udah pasti error dengan ngembaliin kode 400, jadi jangan ekspek hal yang selalu bener, kadang kita emang perlu salah untuk tau itu bener-bener salah!

def test_update_user(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']

new_name = "Kiddy"
response = client.put(f"/users/{id}", json={"name": new_name})
assert response.status_code == 200

user = Users.objects(id=id).first()
assert user.name == new_name

Kita akan test update sebuah user dengan keadaan normal, alias pasti bener, nah di assert kedua, kita pastiin nih, user tersebut udah berubah belom namanya? Makanya saya buat:

assert user.name == new_name

Lanjut:

def test_update_user_with_long_name(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']

new_name = "SngQ8CL1kqtowD4rl1kYrWG2WmLhvB6HQ7exaY3a5fFkG6LPBn4s" \
"Gtc5HZw7QHiQtsLKrAX7G7wBPp5of6utYDeTLllNPMJfE1m"
response = client.put(f"/users/{id}", json={"name": new_name})
assert response.status_code == 200

user = Users.objects(id=id).first()
assert user.name == new_name

Kita test dengan nama panjang, secara logic sih harusnya ngga boleh, ya tugas kalian aja lah ya bikin validasinya gimana biar 400 hehehe.

def test_update_user_with_wrong_id(self):
name = "Hudya"

client.post("/users", json={"name": name})
id = str(ObjectId())

new_name = "Kiddy"
response = client.put(f"/users/{id}", json={"name": new_name})
assert response.status_code == 400

user = Users.objects(id=id).first()
assert user is None

Kita test kalo Idnya salah alias ngasal dengan ngebuat Object ID baru, otomatis pasti salah karena datanya ngga ada sehingga error 400, dan ketika di assert kedua kita bener-bener cek ke DB kalo data usernya emang ngga ada.

def test_update_user_without_name(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']

response = client.put(f"/users/{id}", json={})
assert response.status_code == 400

user = Users.objects(id=id).first()
assert user.name == name

Kalo usernya ngga ngirim JSON object pas update apa yang terjadi? Harus error juga! Nah abis itu kita cek, karena datanya error, maka data lama yang baru kita insert ngga boleh berubah namanya!

def test_update_user_with_empty_name(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']

response = client.put(f"/users/{id}", json={"name": ""})
assert response.status_code == 400

user = Users.objects(id=id).first()
assert user.name == name

Nah terus kita cek kalo dia ngirim string kosong, harus error juga! Abis itu assert yang kedua kita cek seperti test sebelumnya.

def test_delete_user(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']

response = client.delete(f"/users/{id}")
assert response.status_code == 200

user = Users.objects(id=id).first()
assert user is None

Kita coba hapus user, pastiin response harus 200 dan user tersebut bener-bener terhapus dari Database!

def test_delete_user_wrong_id(self):
name = "Hudya"

response = client.post("/users", json={"name": name})

res = response.text
res = json.loads(res)
id = res['values']['id']
fake_id = str(ObjectId())

response = client.delete(f"/users/{fake_id}")
assert response.status_code == 400

user = Users.objects(id=id).first()
assert user.name == name

user = Users.objects(id=fake_id).first()
assert user is None

Sekarang ktia coba hapus user tersebut tapi pake fake_id alias ID yang kaga tau respawn darimana, nah abis itu pastiin response 400 alias error, terus assert kalo user dengan ID itu masih ada dengan cek namenya, lalu cek kalo fake ID itu beneran ngga ada.

Nah kalo djialanin semua jadi gini guys, sehingga terdapatlah 11 test case yang common tapi lumayan nyebelin loh kalo ngga ditest, males banget kan nyobain satu persatu test yang “kemungkinan terjadi” mending buat unit test aja dan biarkan mereka yang bekerja.

Selain itu Unit test bisa jadi patokan ketika kita develop lanjutan projek kita, semua projek kita harus bisa lolos dari test.

Jadi anggap kamu nambahin fitur di bagian user, kamu harus cek kembali semua possibilitynya di test_unit, dan nambahin test-test baru atau modify yang lama, contoh kalo misalnya kamu mau ngebuat attribut baru yaitu email di user, berarti test_insert_user juga harus dibuat untuk memasukkan email, dan bisa dicoba lagi gimana kalo misalnya name dimasukkan tapi email ngga dimasukkan, jadi semakin kompleks program, unit test juga akan semakin beragam casenya, dan developer harus bisa coverage dan menyiapkan itu ^^.

Nah kita juga bisa membuat unit test milik kita terintegrasi dengan repository online seperti Github/Bitbucket/Gitlab.

Nah ini yang disebut dengan CI/CD (Continues Integration/Continues Deployment), dimana kamu dan devops akan menguji coba “kelayakan” software yang kamu buat dengan test unit yang sudah ada, dan setiap didevelop fitur baru, CI/CD ini akan dijalankan untuk mengetes apakah kode kamu sudah layak dirilis dengan environment production.

Kenapa gitu? Ya mungkin aja di local kamu jalan, di staging server alias server uji coba bisa aja ngga jalan loh, kenapa gitu? Bisa aja ada data yang begini-begitu, atau case-case khusus tapi lupa kamu kasih kondisi, ternyata error deh hahahaha. Jadi beda environment beda keadaan ya kawan, pastikan untuk selalu menguji coba sebelum itu benar-benar dideploy, baik automated testing maupun manual testing (human black box testing), sangat dibutuhkan nutuk mencegah bug-bug production yang bikin kepala pening.

Oke kalo gitu, sekian dulu ya dari saya, semoga artikel yang ini dan kemarin menambahkan insight ke kepala-mu~ Jangan lupa untuk terus coding, belajar, dan berkembang, karena kita bukan apa-apa dan bukan siapa-siapa tanpa ilmu!

Akhir kata sekian, and happy coding! Tunggu artikel-artikel backend lainnya hanya di Blog ini~

--

--

Hudya
Hudya

Written by Hudya

Which is more difficult, coding or counting? Not both of them, the difficult one is sharing your knowledge to people without asking the payment.

No responses yet