Posts › Advanced Testing Methods for APIs

2019-06-13

Basic assertion tests and classic integration testing (e.g. via Selenium) methods are generally accepted as popular knowledge; the concepts as well as the code is easy to understand.

What if we had to test an API with endpoints that:

  1. generate responses (i.e. each response is literally different)
  2. return large (broad or deep) data structures as part of responses

It's not immediately obvious how we can test this efficiently. There's a natural gap between keeping test code and tools simple when dealing with more sophisticated services and apps. Fear not! There are literally dozens of us out there who have thought about this and come up with solutions.

The following examples are given on a JS stack, though concepts persist outside of a particular language or technology.

Invariant Testing

Simply put, this style of testing compares the shape of values with an expected shape of the value. Rather than testing for the appearance of specific values, we're testing for the presence or absence of classes of values.

Within Jest, we have advanced matchers to help:

interface IGeoIP {
    country:        string;
    cityName:       string;
    postalRegion:   number;
    provider:       {
        ip: string;
        name: string;
    }
}

...

it('should return an instance of a IGeoIP', () => {
    expect(getRandomGeoIP()).toEqual(expect.objectContaining({
        country:        expect.any(String),
        cityname:       expect.any(String),
        postalRegion:   expect.any(Number),
        provider:       expect.objectContaining({
            ip:     expect.any(String),
            name:   expect.any(String)
        })
    );
});

Any schema validation matching library can be used as a drop-in replacement for invariant testing. yup is a common library used for this purpose. The syntax is similar but it is more feature-rich, additionally, you can store these schemas within your code to be used for normalization as well as testing.

const schema = yup.object().shape({
    country:        yup.string().required(),
    cityName:       yup.string().required(),
    postalRegion:   yup.number().positive().integer().required(),
    provider:       yup.object().shape({
        
        ip: yup.string()
            .matches(/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)
            .required(),
        
        name: yup.string()
            .required()
    })
});

it('should return an instance of a IGeoIP', () => {
    expect(schema.validateSync(getRandomGeoIP())).toBe(true);
});

Snapshot Testing

With expansive responses or values of any sort the task of writing unit tests to cover broad volumes of data is not only tedious, it is very error prone. The concept of snapshot testing takes care of this by serializing such values into a format that is easily parsed and machine or human diff-able.

Running the test initially creates the snapshot file, to which, future test runs are compared. If there is any difference within the serialized content, the test fails.

When the response updates, it can only be due to 1 of 2 possible things:

  1. The code is wrong -- in which case, we must fix it
  2. The correct value has changed -- so we update the snapshot (e.g. jest -u)
it('should return a sorted collection of motor vehicles', () => {
    expect(getSortedMotorVehicles()).toMatchSnapshot();
});

// Inside the snapshot file:

exports[`should return a sorted collection of motor vehicles 1`] = `
Object {
    "vehicles": [
        { 
            "make": "Honda"
            ...
        },
        { 
            "make": "Ford"
            ...
        },
        { ... },
        ...
    }
}
`;

In some cases, you may need to write your own serializer or find an adequate dependency to deal with non-primitive values that Jest cannot serialize correctly (e.g. instances of classes with getters / setters). As long as a primitive value is returned, this concept will work.

Find the best combination of test strategies for you

Cohesive.dev can help find the perfect balance for your development roadmap. Speak to us about making your software platform more stable and reliable.