Struktur Framework dan Fluent Assertion - API Test Framework Python (Bagian 3)
Â
Hi Kawan 👋,
Gimana mudah kan belajar bikin API automation dengan python? tentunya,, di kesempatan ini kita akan belajar lebih lanjut dengan pytest dan menggunakan library tambahan (lagi) agar test kita lebih mudah dibaca dan dipahami dengan bantuan fluent assertionÂ
Artikel ini adalah salah satu dari rangkaian tutorial cara membuat RestfulAPI Test Framework dengan bahasa pemrograman Python, artikel lainnya bisa kamu cekidot disini ya:
- Pelajaran 1: Setup dan pengenalan HTTP requests
- Pelajaran 2: Pengujian dengan assert dan pytest
- Pelajaran 3: Struktur awal test framework dan library fluent assertion
- Pelajaran 4: Refaktoring dan base helper
- Pelajaran 5: Reporting
- Extra: Penambahan schema validation dan running test in parallel
Kenapa kita perlu fluent assertion?
Karena assert terlalu sederhana, dia hanya bisa mengevaluasi value True/False saja,, (eh tapi pada dasarnya semua juga sama sih mau pake library ini pun gitu, hahaha)
Maksudnya ada banyak fitur tambahan dari library ini, bisa dilihat dokumentasinya di web resminya https://github.com/assertpy/assertpy dan secara kode pun akan mudah dibaca dan dipahami kok
Install library ini dengan command `pip install assertpy`
Mengakses API method POST
Oke kali ini kita ga akan pake GET airports lagi sebagai contoh, tapi kita akan menggunakan contoh API POST untuk menghitung jarak antar airports ya, kurang lebih kode nya akan seperti ini
import requests
from assertpy import assert_that
def test_calculate_distance():
payload = {
"from": "LAE",
"to": "NRT"
}
response = requests.post("https://airportgap.dev-tester.com/api/airports/distance", data=payload)
data = response.json().get("data")
assert_that(data["type"]).is_equal_to("airport_distance")
assert_that(data["attributes"]).is_not_empty()
assert_that(data).has_id("LAE-NRT")
assert_that(data["attributes"]["kilometers"]).is_equal_to(4753.834755437252)
 Penjelasan kode, yang beda aja dari artikel sebelumnya ya hehe:
- from assertpy import assert_that ==> melakukan import function assert_that yang berada pada module library assertpy
- payload = {} ==> deklarasi variable dengan tipe data dictionary
- response = requests.post(<url>, data) ==> nah seperti pada umumnya method POST adalah untuk mengirim data, maka kita akan mengirim data payload secara programatis dengan menggunakan argument tambahan data=payload
- assert_that(<condition>).is_equal_to(<expected result>) ==> penggunaan assertpy library untuk assertion, lihatlah kodenya jadi mudah dipahami bukan?
Struktur Framework pytest
Pada umumnya kode pengujian pada sebuah project python terletak pada directory `test` atau `tests`Â jadi untuk standarisasi kita akan mengikuti kaidah ini, sehingga bentuk project test kita menjadi seperti ini
.
└── test
├── airport_test.py
└── favorite_test.py
Oia, sebagai convention pun kita perlu menamakan file kita dengan _test.py agar pytest bisa menemukan file mana saja yang akan dieksekusi sebagai test
Kita membagi dua file utnuk pengujian api yang berkaitan dengan airport dan favorite ke dalam file yang terpisah agar mudah dipahami, jadi semua test yang berkaitan dengan module favorite pasti terletak di favorite_test.py jadi test kita pun bisa menjadi sebuah dokumentasi yang baik
Kurang lebih contoh akhir dari file airport_test.py adalah seperti ini
import requests
from assertpy import assert_that
BASE_URL = "https://airportgap.dev-tester.com/api"
def test_get_all_airports():
response = requests.get(f'{BASE_URL}/airports')
data = response.json().get('data')
assert_that(response.status_code).is_equal_to(200)
assert_that(data).is_greater_than_or_equal_to(5)
def test_get_airports_by_id():
# arrange/GIVEN
airport_id = "LAE"
# action/WHEN
response = requests.get(f'{BASE_URL}/airports/{airport_id}')
data = response.json().get('data')
data_airport = data["attributes"]
# assertion/THEN
assert_that(response.status_code).is_equal_to(200)
assert_that(data_airport["name"]).is_equal_to("Nadzab Airport")
def test_get_not_found_airport():
airport_id = "NOTFOUND"
response = requests.get(f'{BASE_URL}/airports/{airport_id}')
assert_that(response.status_code).is_equal_to(404)
assert_that(response.json()).contains_key("errors")
assert_that(response.text).contains("The page you requested could not be found")
def test_calculate_distance():
payload = {
"from": "LAE",
"to": "NRT"
}
response = requests.post("https://airportgap.dev-tester.com/api/airports/distance", data=payload)
data = response.json().get("data")
assert_that(data["type"]).is_equal_to("airport_distance")
assert_that(data["attributes"]).is_not_empty()
assert_that(data).has_id("LAE-NRT")
assert_that(data["attributes"]["kilometers"]).is_equal_to(4753.834755437252)
dan favorite_test.py
from datetime import datetime
import requests
from assertpy import assert_that
BASE_URL = "https://airportgap.dev-tester.com/api"
def get_token(email, password):
return requests.post(BASE_URL + "/tokens", data={
"email": email,
"password": password
})
def get_my_favorite_airport(token):
header = {
"Authorization": f"Token {token}"
}
return requests.get(BASE_URL + "/favorites", headers=header)
def test_token():
token = get_token(email="[email protected]", password="shuriken")
assert_that(token.status_code).is_equal_to(200)
assert_that(token.json()).contains_key("token")
assert_that(token.json().get("token")).is_not_empty()
def test_get_favorite():
give_me_token = get_token(email="[email protected]", password="shuriken")
token = give_me_token.json().get("token")
header = {
"Authorization": f"Token {token}"
}
response = requests.get(BASE_URL + "/favorites", headers=header)
data = response.json().get("data")
assert_that(response.status_code).is_equal_to(200)
assert_that(data).is_greater_than_or_equal_to(1)
def test_add_new_favorite():
"""
Since the data is hardcoded, then it was not idempotent/repeatable
This test will failed, if NRT already in my favorite list
Need to remove after test teardown
"""
give_me_token = get_token(email="[email protected]", password="shuriken")
token = give_me_token.json().get("token")
before_add = get_my_favorite_airport(token)
data_before = before_add.json().get("data")
assert_that(before_add.text).does_not_contain("NRT")
new_airport = {
"airport_id": "NRT",
"note": "I like Narita"
}
header = {
"Authorization": f"Token {token}"
}
response = requests.post(BASE_URL + "/favorites", headers=header, data=new_airport)
assert_that(response.status_code).is_equal_to(201)
response_data = response.json().get("data")
assert_that(response_data["attributes"]["airport"]["iata"]).is_equal_to(new_airport["airport_id"])
assert_that(response_data["attributes"]["note"]).is_equal_to(new_airport["note"])
after_add = get_my_favorite_airport(token)
data_after = after_add.json().get("data")
assert_that(len(data_after)).is_greater_than(len(data_before))
assert_that(after_add.text).contains(new_airport["note"])
def test_update_favorite_note():
give_me_token = get_token(email="[email protected]", password="shuriken")
token = give_me_token.json().get("token")
before_add = get_my_favorite_airport(token)
data_before = before_add.json().get("data")
assert_that(len(data_before)).is_greater_than(0)
id_airport_to_modify = data_before[0]["id"]
header = {
"Authorization": f"Token {token}"
}
# Add dynamic unique string with a timestamp
new_note = {
"note": f"The note for ID:{id_airport_to_modify} is changed at time: {datetime.now()} "
}
response = requests.patch(f"{BASE_URL}/favorites/{id_airport_to_modify}", headers=header, data=new_note)
assert_that(response.status_code).is_equal_to(200)
assert_that(response.text).contains(new_note["note"])
after_add = get_my_favorite_airport(token)
data_after = after_add.json().get("data")
for airport_data in data_after:
if airport_data["id"] == id_airport_to_modify:
print(f"Found the modified airport {id_airport_to_modify}")
print("\nHere is the check for specific airport in an array data")
assert_that(airport_data["attributes"]["note"]).is_equal_to(new_note["note"])
def test_delete_favorite():
give_me_token = get_token(email="[email protected]", password="shuriken")
token = give_me_token.json().get("token")
before_add = get_my_favorite_airport(token)
data_before = before_add.json().get("data")
assert_that(len(data_before)).is_greater_than(0)
id_airport_to_delete = data_before[0]["id"]
header = {
"Authorization": f"Token {token}"
}
response = requests.delete(f"{BASE_URL}/favorites/{id_airport_to_delete}", headers=header)
assert_that(response.status_code).is_equal_to(204)
after_delete = get_my_favorite_airport(token)
data_after = after_delete.json().get("data")
assert_that(len(data_after)).is_less_than(len(data_before))
Penjelasan kode? hmm sepertinya akan sangat panjang, nanti akan saya jelaskan pada video sepertinya agar lebih runut ehehe, klo nulis keknya panjang ehehe
Bagaimana cara menjalankan kode dengan pytest?
Klo mau menjalankan satu satu sih bisa seperti cara sebelumnya `Â pytest test/airport_test.py`
Tapi klo mau menjalankan seluruh test, karena sudah sesuai convention (dalam folder test, file sudah bearakhiran xxx_test.py dan method berawalan test_xxx) sebenarnya cukup menggunakan `pytest` saja tanpa embel embel loh
Cukup sekian tutorial kali ini, silakan teman-teman pelajari approach saya dalam menulis kode di atas, apakah sudah test nya seperti itu? atau kamu mungkin punya assertion yang bisa jadi ditambahkan juga, silakan tulis di kolom komentar di bawah ya!
 Berikutnya kita akan bahas, bagaimana kode ini bisa kita refactor dengan menggunakan class Base API ya
Salam!
Selengkapnya ada pula di video live coding berikut ini:
Comments