Tutorial RESTful API dengan FastAPI Python + MongoDB Part 2- Unit Test
Menulis Unit Test pada FastAPI Python, Mocking Databasenya biar gampang!
Tutorial RESTful API dengan FastAPI Python + MongoDB Part 2— Unit Test
Menulis Unit Test pada FastAPI Python, Mocking Databasenya biar gampang!
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~
kiddyxyz/restful-fastapi-mongo*You can’t perform that action at this time. You signed in with another tab or window. You signed out in another tab or…*github.com
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 TestCaseimport jsonfrom bson import ObjectIdfrom mongoengine import connect, disconnectfrom starlette.testclient import TestClientfrom app import appfrom 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 == nameDi 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$ pytestCukup eksekusi pytest dan dia otomatis nyari yang ada kata test_xxx.py-nya di folder tests.
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 == nameFungsi 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 == 400Test 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 == 400Nah 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_nameKita 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_nameLanjut:
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_nameKita 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 NoneKita 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 == nameKalo 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 == nameNah 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 NoneKita 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 NoneSekarang 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~
Artikel ini merupakan konten legacy dari blog Medium (Tahun 2020). Beberapa konsep atau sintaks mungkin sudah mengalami perubahan pada versi terbaru.