Dùng Sublime Text 3 để debug API một cách hiệu quả

Bài viết này hướng dẫn cách sử dụng Sublime Text 3 kết hợp với một script Python tùy chỉnh để debug các API, hỗ trợ các tính năng như gửi request (GET, POST, v.v.), xử lý dữ liệu JSON và form data, quản lý headers, cookies, sessions, thực hiện ký tự số và xác thực kết quả trả về.

Tình huống sử dụng

Trong môi trường microservices, việc các API yêu cầu chữ ký số có thể gây khó khăn khi debug bằng các công cụ như Postman. Mỗi lần debug, bạn có thể phải sửa đổi mã nguồn để tạm thời vô hiệu hóa việc kiểm tra chữ ký, điều này ảnh hưởng đến hoạt động của các dịch vụ khác. Ngoài ra, việc định dạng lại các tham số API lấy từ ELK hoặc sao chép thủ công các cặp khóa-giá trị cho các API dạng form data cũng tốn nhiều thời gian.

Nguyên lý hoạt động

  • Sublime Text 3 cho phép tùy chỉnh hệ thống build.
  • Python cung cấp khả năng xử lý dữ liệu JSON mạnh mẽ.
  • Sử dụng Python để triển khai thuật toán ký số.
  • Sử dụng thư viện requests của Python để gửi yêu cầu HTTP.

Mục tiêu

  • Sử dụng file JSON làm cơ sở dữ liệu cho các trường hợp kiểm thử, hỗ trợ một file chứa nhiều API.
  • Hỗ trợ đa dạng các phương thức HTTP (GET, POST, DELETE, PUT, PATCH, HEAD).
  • Hỗ trợ cả request dạng form data và JSON.
  • Hỗ trợ tùy chỉnh headers, cookies, params, data, files, và timeout.
  • Hỗ trợ quản lý phiên làm việc (session) và biến trung gian giữa các request.

Định dạng file dữ liệu (JSON)

Request GET

Có hai cách định dạng:

{
  "url": "http://127.0.0.1:5000/api/user/logout/?name=张三"
}
    
Hoặc
{
  "url": "http://127.0.0.1:5000/api/user/logout/",
  "params": {"name": "张三"}
}
    

Request POST (form data)

{
  "url": "http://127.0.0.1:5000/api/user/login/",
  "data": {"name": "张三","password": "123456"}
}
    

Request JSON

{
  "url": "http://127.0.0.1:5000/api/user/reg/",
  "headers": {"Content-Type": "application/json"},
  "data": { "name": "张991", "password": "123456"}
}
    
Hoặc chỉ định rõ kiểu là 'json':
{
  "url": "http://127.0.0.1:5000/api/user/reg/",
  "type": "json",
  "data": { "name": "张991","password": "123456"}
}
    

Xác thực kết quả (ASSERT)

Cho phép định nghĩa các điều kiện kiểm tra phản hồi của API.

{
  "url": "http://127.0.0.1:5000/api/user/login/",
  "data": {"name": "张三","password": "123456"},
  "ASSERT": ["response.status_code == 200", "'成功' in response.text"]
}
    

Yêu cầu ký số (SIGN)

Tự động thêm tham số chữ ký vào dữ liệu request.

{   "url": "http://127.0.0.1:5000/api/user/delUser/",
    "type": "json",
    "data": {"name": "张三"},
    "SIGN": true
}
    

Nhiều API, phụ thuộc Session và biến trung gian (STORE)

Cho phép thực hiện chuỗi request, lưu trữ kết quả từ request trước để sử dụng cho request sau.

[
	{
	    "url": "http://127.0.0.1:5000/api/user/getToken/",
	    "method": "get",
	    "params": {"appid": "136425"},
	    "STORE": {"token": "response.text.split('=')[1]"}
	},
	{   "url": "http://127.0.0.1:5000/api/user/updateUser/?token=%(token)s",
	    "headers": {"Content-Type": "application/json"},
	    "data": {"name": "张三","password": "123456"}
	}
]
    

Triển khai công cụ

Script sign.py (Xử lý ký số)

Script này định nghĩa hàm makeSign để tạo chữ ký số cho dữ liệu request dựa trên một cấu trúc đã định sẵn.

import hashlib

# Thông tin cấu hình bí mật
APPID = "user" # Giá trị mẫu, cần được cấu hình phù hợp
APPSECRET = "NTA3ZTU2ZWM5ZmVkYTVmMDBkMDM3YTBi" # Giá trị mẫu, cần được cấu hình phù hợp

def calculate_md5(input_string):
    """Tính toán giá trị MD5 cho một chuỗi đầu vào."""
    hasher = hashlib.md5()
    hasher.update(input_string.encode('utf8'))
    return hasher.hexdigest()

def generate_signature(params):
    """
    Tạo chữ ký số cho dữ liệu request.
    Args:
        params (dict): Dữ liệu request dạng từ điển.
    Returns:
        dict: Dữ liệu request đã được thêm trường 'sign', hoặc None nếu có lỗi.
    """
    if not isinstance(params, dict):
        print("Lỗi: Định dạng tham số không hợp lệ, yêu cầu là kiểu dict.")
        return None
    
    # Loại bỏ trường 'sign' nếu có để tính toán lại
    if 'sign' in params:
        params.pop('sign')
        
    signature_string = ""
    # Sắp xếp các khóa theo thứ tự alphabet để đảm bảo tính nhất quán
    for key in sorted(params.keys()):
        signature_string += f"{key}={params[key]}&"
        
    # Thêm appsecret và tính MD5
    signature_string += f"appsecret={APPSECRET}"
    final_signature = calculate_md5(signature_string)
    
    # Thêm chữ ký vào tham số
    params['sign'] = final_signature
    return params
    

Script post_json.py (Gửi request và xử lý kết quả)

Script chính thực hiện việc đọc file JSON, gửi request bằng thư viện requests, xử lý chữ ký, lưu trữ biến và xác thực kết quả.

import json
import os
import requests
import sys
from sign import generate_signature # Sử dụng hàm đã sửa đổi

DEFAULT_TIMEOUT = 10 # Thời gian chờ mặc định cho request

def execute_api_requests(file_path, custom_timeout=DEFAULT_TIMEOUT):
    """
    Đọc file JSON, thực hiện các request API và xử lý kết quả.
    Args:
        file_path (str): Đường dẫn đến file JSON chứa định nghĩa các API.
        custom_timeout (int): Thời gian chờ tùy chỉnh cho request.
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            api_definitions = json.load(f)
    except FileNotFoundError:
        print(f"Lỗi: Không tìm thấy file '{file_path}'.")
        return
    except json.JSONDecodeError:
        print(f"Lỗi: Định dạng JSON trong file '{file_path}' không hợp lệ.")
        return
    except IOError as e:
        print(f"Lỗi I/O khi đọc file '{file_path}': {e}")
        return

    # Đảm bảo api_definitions là một danh sách để dễ dàng lặp
    if isinstance(api_definitions, dict):
        api_definitions = [api_definitions]

    session = requests.Session() # Sử dụng session để quản lý cookies và kết nối
    global_vars = {} # Biến toàn cục để lưu trữ kết quả STORE

    for api_def in api_definitions:
        current_api_data = json.dumps(api_def) # Chuyển về chuỗi để xử lý biến

        # Thay thế các biến toàn cục trong định nghĩa API
        try:
            # Sử dụng format để thay thế biến, tránh lỗi nếu có '%' trong chuỗi
            # Cần cẩn thận với các ký tự đặc biệt nếu dùng %
            # Thay thế bằng cách ép kiểu về dict sau khi format chuỗi
             formatted_api_str = current_api_data.format(**global_vars)
             api_request_config = json.loads(formatted_api_str)
        except KeyError as e:
            print(f"Lỗi: Không tìm thấy biến toàn cục '{e}' để thay thế trong định nghĩa API.")
            continue
        except json.JSONDecodeError:
             print("Lỗi: Định dạng JSON sau khi thay thế biến không hợp lệ.")
             continue
             
        # Trích xuất thông tin request từ cấu hình
        url = api_request_config.get('url')
        method = api_request_config.get('method', '').lower()
        req_type = api_request_config.get('type', '').lower()
        headers = api_request_config.get('headers')
        cookies = api_request_config.get('cookies')
        params = api_request_config.get('params')
        data = api_request_config.get('data')
        files = api_request_config.get('files')

        # Xử lý các hành động đặc biệt
        should_sign = api_request_config.get('SIGN', False)
        store_definitions = api_request_config.get('STORE')
        assertion_definitions = api_request_config.get('ASSERT')

        # Thực hiện ký số nếu được yêu cầu
        if should_sign and data:
            data = generate_signature(data)
            if data is None: # Kiểm tra nếu generate_signature trả về None do lỗi
                print("Lỗi khi tạo chữ ký.")
                continue

        # Chuyển đổi dữ liệu sang JSON nếu 'type' là 'json' hoặc headers có Content-Type là application/json
        is_json_request = (req_type == 'json' or 
                           (headers and 'Content-Type' in headers and 'json' in headers['Content-Type']))
        if is_json_request:
            data = json.dumps(data)

        # Xác định phương thức HTTP và gửi request
        response = None
        try:
            if method == 'post' or (not method and data): # Mặc định là POST nếu có data
                response = session.post(url=url, headers=headers, cookies=cookies, params=params, data=data, files=files, timeout=custom_timeout)
            elif method == 'get' or not data: # Mặc định là GET nếu không có data
                response = session.get(url=url, headers=headers, cookies=cookies, params=params, timeout=custom_timeout)
            elif method == 'delete':
                response = session.delete(url=url, headers=headers, cookies=cookies, params=params, data=data, files=files, timeout=custom_timeout)
            elif method == 'put':
                response = session.put(url=url, headers=headers, cookies=cookies, params=params, data=data, files=files, timeout=custom_timeout)
            elif method == 'patch':
                response = session.patch(url=url, headers=headers, cookies=cookies, params=params, data=data, files=files, timeout=custom_timeout)
            elif method == 'head':
                 response = session.head(url=url, headers=headers, cookies=cookies, params=params, data=data, files=files, timeout=custom_timeout)
            else:
                print(f"Cảnh báo: Phương thức '{method}' không được hỗ trợ hoặc không xác định.")
                continue
        except requests.exceptions.RequestException as e:
            print(f"Lỗi request tới {url}: {e}")
            continue

        # Lưu trữ biến trung gian nếu có định nghĩa STORE
        if store_definitions:
            for var_name, expression in store_definitions.items():
                try:
                    # Đánh giá biểu thức để lấy giá trị và lưu vào biến toàn cục
                    # Cần đảm bảo 'response' object có sẵn trong scope của eval
                    # Thêm các thuộc tính cần thiết của response vào scope eval
                    eval_scope = {'response': response, 'json': json, 'globals': globals()}
                    evaluated_value = eval(expression, eval_scope)
                    global_vars[var_name] = evaluated_value
                except Exception as e:
                    print(f"Lỗi khi lưu biến '{var_name}' từ biểu thức '{expression}': {e}")

        # Xử lý và in kết quả response
        response_output = ""
        try:
            # Ưu tiên định dạng JSON nếu response là JSON
            response_output = json.dumps(response.json(), ensure_ascii=False, indent=2)
        except json.JSONDecodeError:
            # Nếu không phải JSON, lấy nội dung text
            response_output = response.text
        except Exception as e: # Bắt các lỗi khác có thể xảy ra khi xử lý response
             print(f"Lỗi khi xử lý nội dung response: {e}")
             response_output = response.content # Lấy nội dung raw nếu cần

        # Thực hiện xác thực kết quả (ASSERT)
        status = "PASS"
        assertion_results_str = []
        if assertion_definitions:
            for assertion_expr in assertion_definitions:
                try:
                    # Cung cấp response object cho hàm eval để đánh giá biểu thức assert
                    eval_scope = {'response': response, 'json': json}
                    assert eval(assertion_expr, eval_scope)
                    assertion_results_str.append(f"PASS: <{assertion_expr}>")
                except AssertionError:
                    assertion_results_str.append(f"FAIL: <{assertion_expr}>")
                    status = "FAIL"
                except Exception as e:
                    assertion_results_str.append(f"ERROR: <{assertion_expr}>\n{repr(e)}")
                    status = "ERROR"
        
        # In thông tin chi tiết của request và response
        print("=" * 80)
        print("Request:")
        # Chuyển đổi data sang định dạng dễ đọc hơn nếu nó không phải là chuỗi (ví dụ: dict)
        printable_data = data if isinstance(data, str) else json.dumps(data) if data is not None else "None"
        print(f"  URL: {url}")
        print(f"  Method: {method.upper() if method else 'POST'}") # Hiển thị phương thức mặc định nếu không có
        if headers: print(f"  Headers: {headers}")
        if params: print(f"  Params: {params}")
        if data and not is_json_request: print(f"  Data (Form): {printable_data}")
        if data and is_json_request: print(f"  Data (JSON): {printable_data}")
        if files: print(f"  Files: {files}")
        print("-" * 80)
        print("Response:")
        print(response_output)
        
        if assertion_definitions:
            print("-" * 80)
            print("Assertions:")
            for result in assertion_results_str:
                print(f"  {result}")
        print(f"Overall Status: {status}")


def main():
    if len(sys.argv) != 2:
        print("Sử dụng: python post_json.py <đường_dẫn_tới_file_json>")
        sys.exit(1)
    
    json_file_path = sys.argv[1]
    execute_api_requests(json_file_path)

if __name__ == "__main__":
    main()
    

Tích hợp vào Sublime Text 3

Để sử dụng script này trực tiếp từ Sublime Text, bạn cần tạo một hệ thống build tùy chỉnh.

  1. Mở Sublime Text.
  2. Vào menu Tools -> Build System -> New Build System....
  3. Dán đoạn mã cấu hình sau vào file mới mở. Điều chỉnh đường dẫn tới trình thông dịch Python 3 và script post_json.py cho phù hợp với hệ thống của bạn.
  4. Lưu file với tên PostJson.sublime-build.

Cấu hình cho Windows

{
    "cmd": ["python", "C:/path/to/your/post_json.py", "$file"],
    "file_regex": "^[ ] *File \"(...*?)\", line ([0-9]*)",
    "selector": "source.json",
    "encoding": "utf-8" 
}
    
*Lưu ý: Thay đổi `"C:/path/to/your/post_json.py"` thành đường dẫn thực tế của bạn.*

Cấu hình cho Linux & macOS

{
    "cmd": ["/usr/bin/python3", "-u", "/path/to/your/post_json.py", "$file"],
    "file_regex": "^[ ] *File \"(...*?)\", line ([0-9]*)",
    "selector": "source.json",
    "env": {"PYTHONIOENCODING": "utf8"}
}
    
*Lưu ý: Thay đổi `/usr/bin/python3` và `/path/to/your/post_json.py` thành đường dẫn thực tế của bạn.*

Cách sử dụng

  1. Bạn nên cài đặt plugin Pretty JSON trong Sublime Text để dễ dàng định dạng file JSON.
  2. Tạo một file mới trong Sublime Text, lưu với đuôi .json (ví dụ: api_test.json).
  3. Nhập định nghĩa API của bạn vào file JSON theo các định dạng đã mô tả ở trên. Ví dụ:
    {
      "url": "http://127.0.0.1:5000/api/user/login/",
      "data": {"name": "张三","password": "123456"}
    }
                
  4. Nhấn tổ hợp phím Ctrl + B (hoặc Cmd + B trên macOS) để chạy hệ thống build đã cấu hình. Kết quả của request và các bước xác thực sẽ hiển thị ở bảng console của Sublime Text.

Các tính năng bổ sung và hướng phát triển

Dự án có thể được mở rộng thêm các chức năng sau:

  • Tự động phát hiện và thực thi hàng loạt các trường hợp kiểm thử (test case discovery).
  • Tạo báo cáo kiểm thử chi tiết (ví dụ: HTML).
  • Hỗ trợ tham số dòng lệnh để tùy chỉnh hành vi (ví dụ: mức độ chi tiết hiển thị, địa chỉ host, môi trường, timeout, đường dẫn xuất báo cáo/log).
  • So sánh kết quả với dữ liệu trong cơ sở dữ liệu.
  • Hỗ trợ thực thi các lệnh SQL, Shell, hoặc Cmd.
  • Sử dụng asynchronous programming (coroutines) để tăng hiệu suất xử lý đồng thời.
  • Hỗ trợ thực thi phân tán trên nhiều máy.
  • Lập lịch thực thi tự động.
  • Tích hợp vào quy trình CI/CD.

Thẻ: Sublime Text API Testing python JSON REST API

Đăng vào ngày 8 tháng 7 lúc 14:14