5

Testing your JSON API in Ruby with dry-rb

 2 years ago
source link: https://medium.com/@paulgoetze/testing-your-json-api-in-ruby-with-dry-rb-dffb6a9bccdf
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

There’s Different Ways

When it comes to testing your JSON API endpoints in Ruby, you can go for plenty of different approaches. The simplest probably is to check the status code and explicitly assert each value of you response data.

Let’s assume we want to test a GET /todos endpoint, which returns to-do items like this:

Simple JSON response of todo items

Then a simple test for this response could look like this:

An example for testing to-do items in our JSON response

However, this approach is a bit cumbersome for large response bodies and can result in a lot of accesses to nested hash keys, until you get to the value you actually want to check. It will also take you quite some effort to adjust the tests as soon as some keys or values in the JSON response change.

So, instead of checking for each exact value, you could move up one level of abstraction and only check for the right structure and data types in your response body. Again there’s different ways to do that.

You could use JSON Schema files and validate your response against them, as is described here:

The JSON Schema approach works quite well. However, writing JSON Schema files can get a bit tricky and confusing, especially when you have to deal with complex and nested JSON data. There’s ways to handle this complexity, e.g. by splitting up and reusing schemas by referencing them.

But maybe there’s a more legible and maintainable way than JSON Schema validation. Let’s explore the realm of dry-rb for this purpose.

What is dry-rb?

dry-rb is a collection of Ruby libraries, whose goal is to encapsulate common tasks — like data validation, data transformation, Ruby object initialization, and others— in small, reusable, and framework-independent Ruby gems.

For our purpose of validating the structure and types of JSON data it provides us with the dry-validation, dry-schema, and dry-types gems. These gems give us all we need for setting up a schema to validate JSON data of almost any complexity.

So let’s have a look at some validation use cases and let’s see how we can leverage the mentioned dry-rb gems for a simple JSON structure. We will also dig into a more complex one afterwards.

A Simple JSON Response

The dry-schema gem comes with a Dry::Schema module which has a Params method to define schemas for validating hashes. Built on top, it also has a JSON method for defining JSON schemas with coercible data types, like Date or Time. This is the one we want to use for our response validation.

A schema for one of our to-do items from above would look something like this:

The Dry::Schema for a single to-do item

With these definitions we make sure that an id, title, and the done flag is present in our to-do item JSON object — and that each property has the given type.

Instead of required() we can also use optional(), to allow the object to not have this property. Instead of value() we can also put maybe() to allow nil values.

Having this schema defined, we can now run the validation in our test for each to-do item by calling the schema, instead of checking for each value explicitly as we did before:

Running the TodoItemSchema validation

Dry-schema also allows us to nest schemas and to pass schemas as property types. So, let’s build a schema for our entire JSON response:

The schema for the full JSON response

Putting all of this together in our test case we can now validate our response body to match the ResponseSchema:

Validating the full response schema

Of course you might still want to check some of the JSON data’s properties explicitly, e.g. to make sure the right to-do ids are involved.

Nevertheless, using dry-schemas to validate the response structure makes your test code a bit more concise already and also makes it easier to understand what the actual JSON response looks like. You can have a quick look at the involved schemas to get an idea about required or optional properties and their (maybe nullable) types.

A More Complex JSON Response

You might think: “Well, nice little examples, but my real-world JSON data is way more complex. Can I use this for these structures, too?”

I think you can. Let’s see how.

Let’s replace some parts of our simple to-do JSON with more complex ones. Instead of the done flag we will have a done_at timestamp, we will add a created_at timestamp and a priority (which could be one of high, default, or low). We will also assume that we have a collaborative to-do list, where we want to include the creator and assignee for each to-do item. Last, let’s have an arbitrary list of tags that can be assigned for a to-do item. This is how our new JSON response might look like:

More complex JSON data for to-do items

A very basic schema for this response would be something like the following:

Basic schema for the more complex JSON response

Note that we used maybe() for the done_at property which allows to have nil values.

This only validates the presence and the top level type of each property. The dry-types gem that is used by our schema, however, provides us with a number of built-in types, like Types::JSON::DateTime (which will be used under the hood if we type :date_time), Types::Array, and Types::String.enum. We can use these to refine our timestamps and tags types in the schema. In order to create custom types we need to create a Types module and include the dry types. Similar to what we already saw for the simple JSON response, we can also create and reuse sub-schemas for the creator and assignee properties:

Enhanced response schema with specific types

This now looks already more like a real-world use case!

Still, we can go one step further and check for the integrity of certain properties. With dry-validation (which is powered by dry-schema) we have a tool at hand, that allows us to define custom rules for our JSON data on top of our existing schema definitions.

Before we can define custom rules we need to change our schema definition from the Dry::Schema.JSON block to a response class that inherits from the Dry::Validation::Contract class:

Setting up the schema as a contract class

The Dry::Validation::Contract base class comes with a rule method that we can use to raise custom validation failures if certain conditions match.

Inside a rule we have access to a values variable, which holds the value(s) of the configured property for that rule. As soon as we assign a failure message (by calling key.failure("some message")) we will make the validation fail.

Let’s say we want to validate the done_at DateTime to always be after the created_at DateTime, and each tag in tags to only appear once. Then these might be our rule definitions:

Custom additional rules in our schema contract class

You can define whatever rules you can come up with to customize your validation contract and to adjust it to your needs. Dry-rb already provides the most often used data types, but you can also use fully customized ones as we saw in the examples above.

Note that our first ResponseSchema definition, where we made use of Dry::Schema.JSON, gave us an instance of the schema class already. When using the Dry::Validation::Contract we need to create an instance of our ResponseSchema ourselves, before we can call its call() method to validate our data.

For more details and dry-validation features I encourage you to checkout the dry-validation documentation.

Some Assertion Syntactical Sugar 🍬

One issue with directly asserting if a schema validation succeeds is that you won’t see what went wrong if the validation failed. Dry-schema provides detailed error messages, so we can make use of these to debug our failing response validation.

We can fix the visibility issue of failure messages by setting up a small test helper method:

A test helper function to display validation error messages

If the validation fails you will now be pointed to the erroneous properties and you will see a message about what went wrong. On top you will get the original JSON response, so you can spot the validation issue in no time.

Conclusion

We saw that dry-rb gems, specifically the dry-schema and dry-validation gems provide us with some useful tools to make JSON response tests in Ruby more legible and maintainable.

On top of making our JSON response tests more concise, the schemas we set up with this approach also allow us to have a quick overview about the structure and data types of our endpoint’s response.

Schemas can be reused in other schemas and tests. They are easy to change and framework agnostic. So, although I used minitest assertion syntax in my code examples, with some small adjustments the shown approaches can also be applied if you use RSpec or any other Ruby testing library.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK