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:
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.
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);
});
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:
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.
Cohesive.dev can help find the perfect balance for your development roadmap. Speak to us about making your software platform more stable and reliable.