API testing is a critical part of ensuring software quality. It’s not enough to just check if your API returns something; you need to validate that the structure and content of the response are correct. This post dives into how to perform robust API testing with Playwright, focusing on validating complex JSON responses using a helper function.

The Challenge: Complex JSON Responses

Many APIs return complex JSON structures. For example, a weather API might return a nested object containing forecasts, locations, and other metadata. Simply checking the status code isn’t enough; you need to verify that the returned data conforms to the expected schema.

Playwright and Custom Validation

Playwright provides the request fixture, which simplifies making API calls. However, for complex responses, you’ll often want to implement custom validation logic. Let’s look at an example of testing an aviation SIGMETs (Significant Meteorological Information) API endpoint.

test('Test aviation sigmets returns correct response', async ({ request, endpoints }) => {
    const aviation = await request.get(`${endpoints.aviation}${sigmets}`);
    const data = await aviation.json();

    expect(isValidResponse(data), 'Response validation failed, failing the test.').toBe(true);
});

Here, we’re making a GET request to the aviation SIGMETs endpoint and then using a custom function, isValidResponse, to validate the JSON response.

The isValidResponse Helper Function

The isValidResponse function is the heart of our validation strategy. It takes the API response data as input and performs a series of checks to ensure it meets our expectations.

export function isValidResponse(data: FeatureCollection): boolean {
    if (typeof data !== 'object' || data === null || Array.isArray(data)) {
        console.error("Invalid data type. Expected object, got:", typeof data);
        return false;
    }
    if (data.type !== "FeatureCollection" || !Array.isArray(data.features) || data["@context"] === undefined) {
        console.error("Invalid FeatureCollection structure. Check 'type', 'features', and '@context'. Data:", data);
        return false;
    }
    for (const feature of data.features) {
        if (feature.type !== "Feature" || (typeof feature.geometry !== 'object' && feature.geometry !== null) || typeof feature.properties !== 'object' || feature.properties === null) {
            console.error("Invalid Feature structure. Check 'type', 'geometry', and 'properties'. Feature:", feature);
            return false;
        }

        const properties = feature.properties;
        if (typeof properties.id !== 'string' || typeof properties.issueTime !== 'string' || typeof properties.fir !== 'string' && properties.fir !== null || typeof properties.atsu !== 'string' || typeof properties.sequence !== 'string' && properties.sequence !== null || typeof properties.phenomenon !== 'string' && properties.phenomenon !== null || typeof properties.start !== 'string' || typeof properties.end !== 'string') {
            console.error("Invalid Feature properties. Check types. Properties:", properties);
            return false;
        }

        if (feature.geometry !== null) {
            const geometry = feature.geometry;
            if (geometry.type !== 'Point' && geometry.type !== 'LineString' && geometry.type !== 'Polygon' && geometry.type !== 'MultiPoint'
                && geometry.type !== 'MultiLineString' && geometry.type !== 'MultiPolygon' || !Array.isArray(geometry.coordinates)) {
                console.error("Invalid geometry. Check 'type' and 'coordinates'. Geometry:", geometry);
                return false;
            }
            for (const coordinateSet of geometry.coordinates) {
                if (!Array.isArray(coordinateSet)) {
                    console.error("Invalid coordinate set. Expected array. Set:", coordinateSet);
                    return false;
                }
                for (const coordinatePair of coordinateSet) {
                    if (!Array.isArray(coordinatePair) || coordinatePair.length !== 2 || typeof coordinatePair[0] !== 'number' || typeof coordinatePair[1] !== 'number') {
                        console.error("Invalid coordinate pair. Expected [number, number]. Pair:", coordinatePair);
                        return false;
                    }
                }
            }
        }

    }
    return true;
}

Key Improvements and Explanations:

  • Type Safety (TypeScript): The function uses TypeScript’s type annotations (data: FeatureCollection) to clearly define the expected data structure. This helps catch errors early.
  • Comprehensive Checks: The function checks various aspects of the response, including:
    • Overall data type and structure (type, features, @context).
    • Structure of individual features (type, geometry, properties).
    • Data types of feature properties.
    • Structure and data types of geometry coordinates.
  • Clear Error Messages: The console.error statements provide detailed information about validation failures, making it easier to debug issues.
  • Readability: The code is well-structured and easy to understand, making it maintainable and adaptable.

Benefits of this Approach:

  • Increased Test Coverage: Validating the entire response structure ensures that your API is returning the correct data in the expected format.
  • Improved Debugging: Detailed error messages pinpoint the exact location of validation failures.
  • Maintainability: A well-structured validation function makes your tests easier to understand and update.

By combining Playwright’s API testing capabilities with custom validation functions, you can create robust tests that thoroughly check the responses from your APIs. This approach is especially valuable when dealing with complex JSON responses, ensuring the reliability and quality of your backend services. Remember to adapt the isValidResponse function to match the specific structure of your API responses for maximum effectiveness.

Podcast also available on PocketCasts, SoundCloud, Spotify, Google Podcasts, Apple Podcasts, and RSS.

Leave a comment