API Testing
Duy Văn | 07/02/2025
Playwright có thể được dùng để truy cập REST API của ứng dụng, nó có thể gửi thẳng request HTTP mà không cần thông qua trình duyệt.
Hữu dụng với:
- Kiểm thử API cúa server
- Setup trạng thái cho server trước khi chạy test web app
- Kiểm thử điều kiện sau sau khi thực hiện hành động trên web app (ví dụ: kiểm tra dữ liệu đã được lưu vào database sau khi thực hiện hành động đăng ký người dùng trên trang web)
Để hiểu thêm về API, đọc ở đây
APIRequestContext
APIRequestContext là một API của Playwright giúp thực hiện request HTTP. Bình thường khi khởi tạo một context trình duyệt, mỗi context đều sẽ có một APIRequestContext riêng. Để sử dụng APIRequestContext, theo như doc, có thể thông qua browser_context.request hoặc page.request, khi này nó sẽ gọi HTTP request sử dụng cookie của trình duyệt.
Để gọi chay APIRequestContext, có thể thực hiện với api_request.new_context()
Ví dụ
Đầu tiên ta sẽ setup một mock api tại đây điền vào project name và ghi lại URL, đây chính là base_url của API.
Giả sử đặt tên project là test123 thì base_url sẽ là https://test111.free.beeceptor.com
Đầu tiên, cấu trúc của API sẽ có một số endpoint, trong số đó mình hiện tại quan tâm đến một endpoint dành cho HTTP Request kiểu GET và một cho kiểu POST
GET Request
Đầu tiên, với ví dụ cho GET Request thì chúng ta sẽ config để nó yêu cầu lấy vào token là 224477 và trả về một object JS như sau
{
"Records": 1,
"Data": [
{
"Iban": "KW03XOWB4342193622763984236499",
"DepositId": 289,
"VendorId": 23196,
"DepositReference": "2024000289",
"BankName": "Test NBK",
"TotalValue": 1150.500,
"DepositDate": "04/12/2024"
}
]
}
Để thực hiện Request nói chung, đầu tiên chúng ta cần tạo một APIRequestContext, sau đó trong bài test sẽ sử dụng APIRequestContext này để thực hiện request.
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright,) -> Generator[APIRequestContext, None,None]:
request_context = playwright.request.new_context (
base_url = "https://test111.free.beeceptor.com"
)
yield request_context
request_context.dispose()
Để giải thích một chút về code trên, nó lấy từ ví dụ của doc Playwright
- fixture @pytest sinh ra để tạo context, hiểu nôm na là trạng thái - context của bài test mà nó sẽ được sử dụng cho toàn bộ bài test, được lặp đi lặp lại. Nếu có một đoạn code cần thực hiện ở từng bài test một cách y hệt nhau thì ta nhét vào fixture
- Tên function ở đây là
api_request_contextlà tên của fixture, nó sẽ được sử dụng trong các bài test khác, thích gọi thế nào thì gọi, nhưng cơ bản là phải đặt tên có ý nghĩa playwright: Playwrightlà param của fixture, hiểu đơn giản là chúng ta muốn lấy vào param playwright và param này là một instance của class Playwright-> Generator[APIRequestContext, None,None]là kiểu trả về của fixture, nó sẽ trả về một APIRequestContext, None thứ nhất là type generator lấy vào vì chúng ta đang không truyền vào gì cả, None thứ hai là kiểu trả về, ở đây cũng là None do chúng ta không trả về gìplaywright.request.new_contextlà cách để tạo một APIRequestContext, nó sẽ tạo một context mới, và chúng ta cần truyền vào base_url của API mà chúng ta muốn thực hiện request, ngoài ra cũng có thể truyền vào headers chung cho toàn bộ request, hoặc cấu hình proxy, …yield request_contextlà cách để trả về một giá trị, ở đây là request_context, và nó sẽ được sử dụng trong các bài test khácrequest_context.dispose()là cách để xóa context sau khi đã sử dụng xong, giúp giải phóng bộ nhớ
Vậy có thể hiểu, nếu đang nối sang lý thuyết autotest thì các phần tương ứng với Setup và Teardown là:
- Setup:
request_context = playwright.request.new_context (base_url = "https://test111.free.beeceptor.com") - Teardown:
request_context.dispose()
Hay nói chính xác hơn là phân cách bởi yield request_context, cứ ở trên là Setup, còn ở dưới là Teardown
Ngoài ra, đây là syntax Python3, cụ thể và rõ ràng, nếu chúng ta dùng syntax kiểu giản lược thì fixture cũng có thể viết là:
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright):
request_context = playwright.request.new_context(
base_url="https://test111.free.beeceptor.com"
)
yield request_context
request_context.dispose()
Đây đã giản lược đi Type Hint, đổi lại chúng ta mất đi static type check và khó hiểu hơn vì người tiếp theo vào làm việc với code sẽ không hiểu các kiểu dữ liệu vào và ra. Nếu đang làm dự án thì nên ưu tiên kiểu viết đầy đủ
Sau khi có context thì chúng ta sẽ thực hiện bài test bao gồm việc thực hiện request và kiểm tra response
def test_api_call(api_request_context: APIRequestContext) -> None:
header = {
"token" : "224477",
}
test_call = api_request_context.get("/supplier", headers = header)
assert test_call.ok
response = test_call.json()
assert response["Data"][0]["Iban"] == "KW03XOWB4342193622763984236499"
Ở đây, chúng ta sẽ có một function bắt đầu với test_, mục tiêu là để pytest có thể nhận biết được đây là một bài test.
headerlà một dict chứa các header mà chúng ta muốn thêm vào request, ở đây chúng ta thêm vào một headertokenvới gía trị đã quy định trên mock APItest_call = api_request_context.get("/supplier", headers = header)là cách để thực hiện request, ở đây chúng ta thực hiện một GET request tới endpoint/suppliervới bộ headers làheaderassert test_call.oklà cách để kiểm tra xem request có thành công hay không, nếu thành công thìtest_call.oksẽ trả về True, nếu không thì sẽ trả về False. Các gía trị HTTP status trong khoảng 200-299 sẽ được coi là thành công- Sau đó chúng ta sẽ kiểm tra dữ liệu trả về trong response, ở đây chúng ta muốn check mã Iban.
response = test_call.json()là cách để chuyển response từ dạng text sang dạng json, để chúng ta có thể truy cập vào các key trong responseassert response["Data"][0]["Iban"] == "KW03XOWB4342193622763984236499"là cách để kiểm tra xem mã Iban trả về có đúng với mã Iban mà chúng ta đã quy định trước không
Tổng hợp lại, toàn bộ code với các dependencies cần thiết sẽ như sau:
from typing import Generator
import pytest
from playwright.sync_api import Playwright, APIRequestContext
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright,) -> Generator[APIRequestContext, None,None]:
request_context = playwright.request.new_context (
base_url = "https://test111.free.beeceptor.com"
)
yield request_context
request_context.dispose()
def test_api_call(api_request_context: APIRequestContext) -> None:
header = {
"token" : "224477",
}
test_call = api_request_context.get("/supplier", headers = header)
assert test_call.ok
response = test_call.json()
assert response["Data"][0]["Iban"] == "KW03XOWB4342193622763984236499"
POTS Request
Tiếp theo với POST Request, chúng ta vẫn giữ nguyên fixture và thay đổi api.request_context.get thành api.request_context.post và thêm vào body cho request
POST của chúng ta sẽ là vào endpoint todos, headers vẫn sẽ cần token, và body sẽ chứa id, response trả về sẽ có dạng
{
"status": "Awesome!",
"id" : "123"
}
Với id trả về chính là id mà chúng ta đã gửi lên
Chúng ta sẽ kiểm tra xem response của nó có đúng id mà chúng ta đã gửi lên không
def test_api_call(api_request_context: APIRequestContext) -> None:
header = {
"token" : "224477",
}
datas = {
"id" : 123
}
test_call = api_request_context.post("/todos", headers = header,data = datas)
assert test_call.ok
response = test_call.json()
assert response["id"] == "123"
Nếu nhét cả hai test vào với nhau, chúng ta có thể đẩy header lên trên fixture
Khi này hai bài test như sau
from typing import Generator
import pytest
from playwright.sync_api import Playwright, APIRequestContext
@pytest.fixture(scope="session")
def api_request_context(playwright: Playwright,) -> Generator[APIRequestContext, None,None]:
header = {
"token" : "224477",
}
request_context = playwright.request.new_context (
base_url = "https://duy247.free.beeceptor.com" , extra_http_headers= header
)
yield request_context
request_context.dispose()
def test_api_call(api_request_context: APIRequestContext) -> None:
header = {
"token" : "224477",
}
test_call = api_request_context.get("/supplier")
assert test_call.ok
response = test_call.json()
assert response["Data"][0]["Iban"] == "KW03XOWB4342193622763984236499"
def test_api_call_2(api_request_context: APIRequestContext) -> None:
datas = {
"id" : 123
}
test_call = api_request_context.post("/todos", data = datas)
assert test_call.ok
response = test_call.json()
assert response["id"] == "123"