Writing Test Cases

The operations section of your Drift test file is where you define individual test scenarios. Each entry describes a specific interaction with your API and the expected outcome. This guide covers everything from minimal test cases to advanced scenarios.

The Minimal Test Case

The simplest Drift test requires only two things: a target and an expected status code.

# yaml-language-server: $schema=https://download.pactflow.io/drift/schemas/drift.testcases.v1.schema.json
drift-testcase-file: v1

operations:
  getAllProducts_Success:
    target: source-oas:getAllProducts
    expected:
      response:
        statusCode: 200

What Drift does automatically:

  • Reads the request/response schema from your OpenAPI spec.

  • Uses the first available example for request bodies (if required).

  • Validates the response against the JSON schema.

  • Checks the content type matches the spec.

This minimal approach is perfect for "happy path" tests where you trust your OpenAPI examples.

Targeting Operations

To test an endpoint, you must point Drift to the correct operation in your source specification.

If your OpenAPI spec defines an operationId, use it directly:

target: source-name:getAllProducts

If an operationId is missing, target the endpoint using the method and path exactly as they appear in the OpenAPI description:

# Format: source:method:path
target: product-oas:get:/products/{id}

Request and Response Bodies

Drift is designed to minimize boilerplate by leveraging your OpenAPI specification.

Table 10. Supported Body Formats

Content type

Support

Notes

application/json

Full support

Parsed as structured data and validated with JSON Schema support.

application/xml

Supported

Enabled through the xml-plugin.

application/x-www-form-urlencoded

Supported

Enabled through the form-urlencoded-plugin.

text/plain

Supported

Sent and validated as plain text.



If your test suite uses XML or form-encoded bodies, include the relevant plugin in the plugins section of your Drift file.

plugins: 
- name: oas  
- name: json  
- name: xml  
- name: form-urlencoded

XML Body Example

XML request bodies must be sent as structured content. See the XML plugin for the full syntax reference, including attribute mapping and supported schema features.

operations:  
    createPet_Success:   
         target: petstore-oas:addPet   
         description: "Create a new pet with a valid payload"    
         parameters:      
            headers:        
                content-type: application/xml      
            request:       
                body:         
                    Pet:            
                        id: 99            
                        name: "bluey"           
                        photoUrls:              
                            - "https://example.com/pets/bluey.jpg"            
                        status: "available"    
    expected:      
        response:        
            statusCode: 200

Form-Encoded Body Example

For application/x-www-form-urlencoded bodies, provide fields as a map under body. See the Form URL Encoded plugin for full details.

operations:
  updatePet_FormEncoded:
    target: petstore-oas:updatePet
    parameters:
      request:
        body:
          name: bluey-updated
          status: sold
    expected:
      response:
        statusCode: 200

Omission Logic (Leveraging Examples)

If your OpenAPI spec includes examples for a request or response body, you can omit the body field in yaml.

Drift will automatically pick the first example found in the specification to use for the test. When you need to test specific scenarios (like a specific product ID or type), use a dataset to provide the body:

parameters:
  request:
    body: ${product-data:products.product10}

Assertions and Expected Outcomes

The expected block defines what a "pass" looks like.

expected:
  response:
    statusCode: 200
    body: ${equalTo(product-data:products.product10)}

The equalTo(value) matcher performs a deep equality check against the specified value.

For non-JSON bodies (e.g., images, PDFs), Drift performs a byte comparison to ensure the provider's output matches the expected source.

Testing Negative Scenarios

Most APIs need validation for error cases like missing authentication, invalid input, or non-existent resources. These "negative tests" are critical for ensuring robust error handling.

Testing Unauthorized Access (401)

To test authentication failures, you need to override or exclude the global authentication:

operations:
  # Using exclude to remove global auth
  getAllProducts_Unauthorized:
    target: source-oas:getAllProducts
    description: "Get all products with invalid authorization"
    exclude:
      - auth
    parameters:
      headers:
        authorization: "Bearer invalid-token"
    expected:
      response:
        statusCode: 401

The exclude field removes globally applied configuration (like authentication headers), allowing you to test what happens when auth is missing or invalid.

Testing Bad Request (400)

Test invalid input by providing malformed data:

operations:
  getProductByID_InvalidID:
    target: source-oas:getProductByID
    description: "Get a product with invalid ID format"
    parameters:
      path:
        id: "invalid"  # Send string where integer expected
    expected:
      response:
        statusCode: 400

When testing bad requests, you're deliberately sending invalid data to verify the API rejects it. However, Drift will normally validate your request against the OpenAPI schema and report errors when you send malformed data.

To silence schema validation errors for bad request tests, use the ignore: schema field:

operations:
  createProduct_MissingRequired:
    target: source-oas:createProduct
    description: "Create product without required fields"
    parameters:
      request:
        body:
          price: 9.99  # Missing required 'name' field
      ignore:
        schema: true  # Ignore request schema validation errors
    expected:
      response:
        statusCode: 400

This prevents Drift from reporting schema validation errors for intentionally invalid requests while still validating the response schema, making it useful for testing validation errors, malformed inputs, and API error handling.

Testing Not Found (404)

Verify the API returns 404 for non-existent resources:

operations:
  getProductByID_NotFound:
    target: source-oas:getProductByID
    description: "Get a product that does not exist"
    parameters:
      path:
        id: 99999  # ID that doesn't exist
    expected:
      response:
        statusCode: 404
        

The exclude field removes globally applied configuration (like authentication headers), allowing you to test what happens when auth is missing or invalid.

Forbidden Access (403)

Test authorization (not authentication) by using a valid token with insufficient permissions:

operations:
  deleteProduct_Forbidden:
    target: source-oas:deleteProduct
    description: "Delete product with read-only token"
    parameters:
      headers:
        authorization: "Bearer ${functions:readonly_token}"
      path:
        id: 10
    expected:
      response:
        statusCode: 403
        

The exclude field removes globally applied configuration (like authentication headers), allowing you to test what happens when auth is missing or invalid.

Using Global Configuration with exclude

Global configuration lets you define common settings once and apply them to all operations.

Defining Global Authentication

global:
  auth:
    apply: true  # Automatically applies to all operations
    parameters:
      authentication:
        scheme: bearer
        token: ${functions:bearer_token}

Excluding Global Configuration

When testing negative cases, use exclude to remove specific global settings:

operations:
  createProduct_Unauthorized:
    target: source-oas:createProduct
    description: "Create product without authentication"
    exclude:
      - auth  # Don't apply the global auth config
    parameters:
      headers:
        authorization: "Bearer invalid-token"
    expected:
      response:
        statusCode: 401

Using exclude keeps test files DRY, clearly indicates when a test intentionally differs from the default setup, and prevents unwanted configuration from being inherited.

Organizing Tests with Tags

Tags help you categorize and selectively run subsets of your test suite.

Adding Tags to Operations

operations:
  getAllProducts_Success:
    target: source-oas:getAllProducts
    tags:
      - smoke
      - products
      - read-only
    expected:
      response:
        statusCode: 200

  createProduct_Success:
    target: source-oas:createProduct
    tags:
      - products
      - write
    expected:
      response:
        statusCode: 201

  getProductByID_Unauthorized:
    target: source-oas:getProductByID
    tags:
      - security
      - auth
    exclude:
      - auth
    expected:
      response:
        statusCode: 401

For more details about CLI tag filtering behavior, see Debugging Test Cases.

Running Tests by Tag

# Run only smoke tests
drift verifier --test-files drift.yaml --tags smoke

# Run all security tests
drift verifier --test-files drift.yaml --tags security

# Run tests with multiple tags (OR logic)
drift verifier --test-files drift.yaml --tags products,write

# Exclude certain tags (NOT logic)
drift verifier --test-files drift.yaml --tags '!security'

Common tag strategies:

  • By functionality: productsusersorders

  • By test type: smokeintegrationregression

  • By stability: stableflakyexperimental

  • By concern: securityperformancevalidation

  • By mutation level: read-onlywritedestructive

Controlling Test Execution Order

By default, Drift executes operations in alphanumeric order by their keys. This works for most stateless APIs, but sometimes you need to control the order — such as when setting up or cleaning up state, or when tests depend on each other.

Using the sequence Field

You can add an optional sequence field to any operation to control its execution order: negative values run first (lowest to highest), followed by unsequenced operations ordered by key, then zero or positive values in ascending order, with ties resolved alphabetically by key.

Example: Setup, Main, and Cleanup

operations:
  setupDatabase:
    target: source-oas:setupDatabase
    description: "Prepare database for tests"
    sequence: -10  # Runs first
    expected:
      response:
        statusCode: 200

  createProduct:
    target: source-oas:createProduct
    description: "Create a product"
    # No sequence: runs after negative, before positive
    expected:
      response:
        statusCode: 201

  getProduct:
    target: source-oas:getProduct
    description: "Get the created product"
    # No sequence: runs after negative, before positive
    expected:
      response:
        statusCode: 200

  cleanup:
    target: source-oas:cleanup
    description: "Clean up test data"
    sequence: 10  # Runs last
    expected:
      response:
        statusCode: 204

Grouping Operations

If you assign the same sequence number to multiple operations, they form a group. The group runs in sequence order, and within the group, operations are ordered by key:

operations:
  stepA:
    sequence: 1
    # ...
  stepB:
    sequence: 1
    # ...
  stepC:
    sequence: 2
    # ...

Here, stepA and stepB run together (ordered by key), then stepC.

Use sequence for setup or teardown steps, state-dependent tests, and cleaner ordering without numeric key prefixes; for more advanced workflows or complex dependencies, use Lua scripting or integrate with your test framework.

Complete Example

Here's a comprehensive test suite combining all these concepts:

# yaml-language-server: $schema=https://download.pactflow.io/drift/schemas/drift.testcases.v1.schema.json
drift-testcase-file: v1
title: "Product API Tests"

sources:
  - name: source-oas
    path: ../openapi.yaml
  - name: product-data
    path: product.dataset.yaml
  - name: functions
    path: product.lua

plugins:
  - name: oas
  - name: json
  - name: data

global:
  auth:
    apply: true
    parameters:
      authentication:
        scheme: bearer
        token: ${functions:bearer_token}

operations:
  # Happy path - minimal test
  getAllProducts_Success:
    target: source-oas:getAllProducts
    tags:
      - smoke
      - read-only
    expected:
      response:
        statusCode: 200

  # Using dataset for specific data
  createProduct_Success:
    target: source-oas:createProduct
    tags:
      - products
      - write
    dataset: product-data
    parameters:
      request:
        body: ${product-data:products.product10}
    expected:
      response:
        statusCode: 201

  # Using OpenAPI examples (body omitted)
  createProduct_SuccessWithExample:
    target: source-oas:createProduct
    description: "Create a product using OpenAPI example"
    tags:
      - smoke
    expected:
      response:
        statusCode: 201

  # Negative test - unauthorized
  createProduct_Unauthorized:
    target: source-oas:createProduct
    description: "Create product with invalid token"
    tags:
      - security
      - auth
    exclude:
      - auth
    parameters:
      headers:
        authorization: "Bearer invalid-token"
      request:
        body:
          id: 20
          name: "test product"
    expected:
      response:
        statusCode: 401

  # Negative test - not found
  getProductByID_NotFound:
    target: source-oas:getProductByID
    description: "Get non-existent product"
    tags:
      - products
    parameters:
      path:
        id: 99999
    expected:
      response:
        statusCode: 404

  # Negative test - bad input
  getProductByID_InvalidID:
    target: source-oas:getProductByID
    description: "Get product with invalid ID format"
    tags:
      - validation
    parameters:
      path:
        id: "invalid"
    expected:
      response:
        statusCode: 400

When Declarative Tests Aren't Enough

The examples above work great when your API is stateless or when test data already exists. But what happens when tests depend on specific system state?

The State Problem

Consider this test that expects a product to exist:

operations:
  deleteProduct_Success:
    target: source-oas:deleteProduct
    description: "Delete an existing product"
    parameters:
      path:
        id: 10  # Assumes product ID 10 exists!
    expected:
      response:
        statusCode: 204

If product ID 10 doesn't exist the test will fail unpredictably. For scenarios requiring setup or cleanup, use lifecycle hooks in Lua scripts:

-- product.lua
local exports = {
  event_handlers = {
    ["operation:started"] = function(event, data)
      -- Create test product before the operation runs
      local res = http({
        url = "http://localhost:8080/products",
        method = "POST",
        body = json.encode({id = 10, name = "Test Product"})
      })
    end,

    ["operation:finished"] = function(event, data)
      -- Clean up after the test completes
      http({
        url = "http://localhost:8080/products/10",
        method = "DELETE"
      })
    end
  }
}

return exports

Common Use Cases for Hooks

  • Creating prerequisite data before tests run

  • Cleaning up test data to ensure test isolation

  • Authenticating and refreshing tokens dynamically

  • Polling for async operations to complete

  • Seeding databases with known test states

  • Resetting state between test runs

Table 11. When to Use Hooks vs. Declarative Tests

Scenario

Approach

API is stateless (read-only endpoints)

Declarative tests

Test data exists and is stable

Declarative tests

Need to create/modify data before testing

Use hooks

Need cleanup between tests

Use hooks

Need dynamic values (timestamps, UUIDs)

Use hooks or data expressions

Testing race conditions or timing

Use hooks



Learn more: See the complete guide on Lifecycle Hooks for detailed examples and available event types.

Next Steps

Use datasets to manage complex test data

Add lifecycle hooks for setup and teardown

Configure authentication for secured endpoints

Explore data expressions for dynamic test generation

Publication date: