aomaker API Automation Testing Framework

repository·master·Indexed 43 days ago

https://github.com/ae86sen/aomaker

An enterprise-grade API automation testing framework that treats 'Documentation as Code' and 'Definition as Test'. It uses declarative modeling with the attrs library to turn API definitions into typed Python objects and supports one-click model generation from OpenAPI 3.x and Swagger 2.0. Key features include automatic JSON Schema validation, a pluggable middleware system, parallel testing via arun, and a built-in SQLite-based storage system for configurations and caching.

Tokens
5.1K
Snippets
21
Records
25
Agent score
5%

What's inside aomaker

  1. Core features of aomaker

    master

    aomaker provides a comprehensive toolchain for enterprise-grade API automation:

    • Declarative Modeling: Uses attrs for clear, type-safe API definitions.
    • Doc-Driven Development: Automatic model generation from OpenAPI/Swagger.
    • Automatic JSON Schema Validation: Automatically validates response structures against the defined model after every request.
    • Middleware System: Allows custom logic (logging, retries, performance stats) before/after requests.
    • Authentication Management: Supports multiple auth methods and dynamic header overrides.
    • Parallel Testing: Supports multi-threading and multi-processing with various task distribution strategies.
    • Storage & Cache: Built-in SQLite-based management for config, cache, schema, and statistics.
    • Ecosystem Tools:
      • Mock Server: For local testing and debugging.
      • Dashboard: Web UI for monitoring execution and logs.
      • CLI: For scaffolding, running tests, and generating models.
      • Reporting: Optimized Allure and aomaker-specific reports.
  2. Manage storage with aomaker.db

    master

    aomaker uses a SQLite database (aomaker.db) for lightweight, thread-safe storage of configurations, caches, and schemas. It contains four core tables:

    TableLifecycleContentUse Case
    configPersistentGlobal configurationHost URLs, account info
    cacheSession-basedTemporary variablesPassing dependency params between steps
    schemaPersistentJSON Schema of responsesAutomatic response validation
    statisticsPersistentInterface metadataData visualization/reporting

    Global Config Usage

    from aomaker.storage import config
    
    def test_env_config():
        current_env = config.get("env")
        test_host = config.get("host")

    Cache Usage

    from aomaker.storage import cache
    
    def setup():
        cache.set("auth_token", "Bearer xxxxx")
    
    def test_api_call():
        headers = {"Authorization": cache.get("auth_token")}
  3. How interface modeling works in aomaker

    master

    aomaker uses a declarative, object-oriented modeling approach to define APIs. Instead of hardcoding URLs and parameters, you define each API as a Python class using the attrs library. This allows you to structure path parameters, query parameters, request bodies, and response bodies into typed objects, providing full IDE autocomplete and type checking.

    Key components of an API model include:

    • PathParams: A nested class defining variables in the URL path.
    • QueryParams: A nested class defining URL query parameters.
    • Response Model: A type hint (often using BaseAPIObject[ResponseClass]) that defines the expected structure of the API response.
    • router: A decorator used to bind the class to a specific HTTP method and path.
    from attrs import define, field 
    from aomaker.core.router import router
    from aomaker.core.api_object import BaseAPIObject
    
    @define(kw_only=True)  
    @router.get("/api/{namespace}/containers")  
    class GetContainersAPI(BaseAPIObject[ContainersResponse]):  
        """Get container list"""
      
        @define  
        class PathParams:  
            namespace: str = field()  
      
        @define  
        class QueryParams:  
            offset: Optional[int] = field(default=0)  
            limit: Optional[int] = field(default=10)  
            name: Optional[str] = field(  
                default=None, metadata={"description": "Container name, fuzzy search"}  
            )  
      
        path_params: PathParams  
        query_params: QueryParams = field(factory=QueryParams)  
        response: Optional[ContainersResponse] = field(  
            default=ContainersResponse  
        )
  4. Define API models using decorators and attrs

    master

    In aomaker, an interface is defined as a class combined with decorators. Use @router.* to declare the route and method, and inherit from BaseAPIObject to enable IDE autocompletion for response types.

    Core parameters for an interface class:

    • path_params: Replaces {} placeholders in the route.
    • query_params: Query string parameters.
    • request_body: The request payload.
    • response: The expected response model.
    from attrs import define, field
    from aomaker.core.router import router
    from aomaker.core.api_object import BaseAPIObject
    
    @define(kw_only=True)
    @router.get("/api/users/{user_id}")
    class GetUserAPI(BaseAPIObject[UserResponse]):
        
        @define
        class PathParams: 
            user_id: int = field()
        
        path_params: PathParams
        response: UserResponse = UserResponse
  5. Create a custom RequestConverter

    master

    If your API documentation's request format (URL/Body) differs from the actual live network traffic (e.g., due to a Microservice Gateway, Encryption/Signing, or required Public Parameters), you can use a RequestConverter to translate the interface model into the required format.

    How to use:

    1. Inherit from RequestConverter: Override the post_prepare hook to transform the PreparedRequest.
    2. Define a Base API Class: Create a class inheriting from BaseAPIObject[T] and set the converter field to your custom class.
    3. Inherit from the Base API: All specific API implementations should inherit from your custom base class.

    This ensures 100% fidelity to real traffic and allows you to maintain gateway rules in a single location.

    # 1. Inherit RequestConverter
    @define
    class GatewayConverter(RequestConverter):
        def post_prepare(self, req: PreparedRequest) -> PreparedRequest:
            body = {"params": json.dumps(req.request_body or {}), "method": req.method}
            return PreparedRequest(
                method="POST",
                url=f"{self.base_url}/global_api/",
                params={"action": self.route},
                request_body=body,
                headers=req.headers,
            )
    
    # 2. Declare a base class
    @define
    class GatewayAPI(BaseAPIObject[T]):
        converter = field(default=GatewayConverter)
    
    # 3. All interfaces inherit GatewayAPI
    class GetContainersAPI(GatewayAPI[ContainersResp]): 
        ...
  6. Generate API models from OpenAPI/Swagger

    master
    aomaker V3 supports one-click generation of interface definition models from OpenAPI 3.x or Swagger 2.0 documentation. This ensures that your test models are always in sync with the actual API implementation and eliminates the need to manually write complex attrs models for large projects.
  7. Create an aomaker scaffold project

    master

    Use the aomaker create command to initialize a new project scaffold. After creation, navigate into the project directory to begin working.

    # 创建脚手架
    aomaker create xxx
    # 进入脚手架项目
    cd xxx
  8. Generate API models from OpenAPI/Swagger documentation

    master

    You can automatically generate Python model definitions from an OpenAPI/Swagger specification (URL or local JSON/YAML file). The generated code will be placed in the directory specified by the -o flag.

    # -s specifies the spec location (URL or local file)
    # -o specifies the output directory
    aomaker gen models -s http://127.0.0.1:9999/api/aomaker-openapi.json -o apis/mock2
  9. Implement custom middleware

    master

    You can inject custom logic (logging, mocking, retries, performance monitoring) into the "request → response" lifecycle using middleware.

    Key Concepts:

    • Pluggable: Any callable with the signature callable(request, next) -> response can be a middleware.
    • Configurable: Use middlewares/middleware.yaml to enable, disable, or reorder middleware without changing code.
    • Execution Order: When running arun, the framework scans the middlewares/ directory and executes them based on their priority and options.

    Implementation Steps:

    1. Define the middleware using the @middleware decorator.
    2. Configure the middleware in middlewares/middleware.yaml.

    Common Use Cases:

    • Structured Logging: Use the built-in logging_middleware.
    • Mocking: Intercept specific URLs to return a CachedResponse.
    • Performance Monitoring: Record time differences and alert on slow thresholds using options: {slow_threshold: 1.0}.
    • Retries: Catch exceptions or specific status codes to retry the next call.
    # middlewares/retry.py
    from aomaker.core.middlewares.registry import middleware, registry
    
    @middleware(name="retry", priority=800, enabled=True,
                options={"max_retries": 3, "codes": [500, 502, 503]})
    def retry_mw(request, call_next):
        for _ in range(registry.middleware_configs["retry"].options["max_retries"]):
            resp = call_next(request)
            if resp.status_code not in registry.middleware_configs["retry"].options["codes"]:
                return resp   # Success
        return resp            # Failed after retries
    # middlewares/middleware.yaml
    logging_middleware: # Built-in
      enabled: true
      priority: 900
    
    retry:         # Name must match the decorator name
      enabled: true
      priority: 800
      options:
        max_retries: 3
        codes: [500, 502, 503]
  10. Start the aomaker mock server

    master

    To use the built-in mock interfaces provided by the scaffold, start the mock server with the --web flag. This allows you to view the interface documentation via a web interface.

    aomaker mock start --web