If you’ve read some of our articles on this site, you might’ve noticed some (if not all) skip writing tests for the code we publish. This is only because there’s so much you can write in an article before it turns into an essay. It’s important to note that writing tests for your code is extremely important. When you’re just playing around with something, writing some proof-of-concept, or something similarly simple, skipping tests might be acceptable. But once your project starts turning into something bigger, where stability is important, writing tests is crucial. In this article, we’d like to go through the different approaches to testing APIs in Ruby, from unit tests to integration tests. We will of course not be able to cover every single way of testing using Ruby, but this should give you a good introduction to the world of testing.
What is Testing?
Testing is, kind of like the word describes, writing code that will test your code. For example, if you write an endpoint for your API, and you want to make sure it does what you expect, you could manually test it (using cURL in the command line, for instance). This is totally fine, and actually expected when you’re just writing the endpoint for the first time, but eventually, you’ll want to automate this. What happens, for example, if you write another endpoint, which drives you to modify something the first endpoint depended on? You could accidentally break the first endpoint. You could, again, manually test every endpoint every time you make a change. What about edge cases? That is, what happens if the user sends some funky data? Can your endpoint handle that? You can see how the list of cases starts piling on, and manually testing every single case every time you want to add or change code will get tedious and time-consuming.
The solution to this problem is to automate your tests. This is what code testing is: writing down all of these test cases so code will test for you. Every time you write a new endpoint, you write test cases to make sure everything works as expected. So the next time you add a new endpoint or modify an existing one, you can be sure (and quickly so) that you didn’t break anything, because your automated tests will tell you if something is behaving unexpectedly. Every time you find a bug, you can write a test to make sure it never appears again.
What types of tests are there?
Your tests are, of course, only as good as what you write. You have to make sure you cover all (or at least most) of the cases, to make sure introducing bugs is as difficult as possible. For this purpose, there are tons of different testing paradigms, frameworks, and libraries. You’re in charge of picking whatever is best for your use case. Ruby on Rails, for example, comes with a predefined set of tests to help you get started, and they should cover most of what a Rails application would need. It’s a combination of unit tests and integration tests.
Unit Tests
Unit tests are defined as tests that will only encompass a very specific unit of code (a function, for example). They will test input and output. You would usually write these out as you’re writing the functions, to make sure they behave as expected. You would also make sure you catch corner cases.
def add(left, right) left + right end RSpec.describe 'add' do it 'adds' do expect(add(1, 2)).to eq(3) end it 'handles negative numbers' do expect(add(-1, -2)).to eq(-3) end it 'handles zeroes' do expect(add(100, 0)).to eq(100) end end
This very crude example shows how you would write some example cases for the add
method. A more real example should test all reasonable corner cases as well.
Integration Tests
Integration tests take it a step further. As the name implies, they test how different components in your program interact and integrate with each other. Unit tests alone cannot test that, and without integration tests, it would be easy to call one function from another function with incorrect parameters. Integration testing would catch mistakes like this.
How to test an API with Ruby
We’d like to show you how to get started with testing your apps. This will, of course, strongly depend on what you’ve written, which frameworks you’ve used, etc., but it should always be possible to write tests to make sure you’re writing stable code. For Ruby, there are tons of testing frameworks to choose from. Some of them are based on specific principles of writing tests. For, example, MiniTest, a small and fast framework, focuses on letting you write simple, readable, and predictable tests. RSpec, on the other hand, is more of a hands-off framework that gives you many tools to write tests however suits you best. They are both great frameworks and work very well, and it’s just a matter of trying them both to see which one you like best.
For this article, we will be focusing on RSpec, but keep in mind most of what you’ll learn here applies to all testing frameworks. You will need to have Ruby installed for this. Follow the official installation instructions to get started, if you don’t already have it installed in your system. We will also be using Rails as a starting point since it helps you get started by generating a bunch of tests for you (or at least the bare minimum). Make sure you install Rails by running gem install rails
on your terminal.
Step 1: Generate a new project
We’re going to be building a simple blog API, which has a Post model and some endpoints to manage them. You can later extrapolate what you learn here to add more models and endpoints. Generate our new project by running this on your terminal:
rails new blog --api --skip-test
Since we’re only focusing on building an API, we’re using the --api
modifier, to skip unnecessary libraries for the frontend. We also skipped installing tests, since we’re going to use RSpec for them. If you’re wondering why we’re choosing RSpec over Rails’ built-in test framework, it’s because we believe RSpec will allow you to use the knowledge you learn here to write tests for other non-Rails projects. This doesn’t mean you can’t use other frameworks for testing, of course. To install RSpec, we’re going to use the rspec-rails gem, which simplifies the installation process. Add the gem to the Gemfile first:
group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'rspec-rails', '~> 4.0.1' end
Make sure you add the gem in the development and test group, otherwise it won’t work correctly. After that, run bundle install
in your terminal, followed by rails generate rspec:install
to finish. This last command will create a couple of files and folders. You should then be good to go.
Step 2: Create our model
Our blog API needs posts. For this, we’ll create a model to hold them. This will be a simple model, where we’ll have some validations. We’ll also take the opportunity to run the scaffold generator, which will generate most of the endpoints for our model in one go. Run rails generate scaffold Post title:string body:text
. You should see something like this as the output:
invoke active_record create db/migrate/20201218135605_create_posts.rb create app/models/post.rb invoke rspec create spec/models/post_spec.rb invoke resource_route route resources :posts invoke scaffold_controller create app/controllers/posts_controller.rb invoke resource_route invoke rspec create spec/requests/posts_spec.rb create spec/routing/posts_routing_spec.rb
As you can see, not only did the command generate our controller and routes, it also generated some test files for us. Some of these files even have tests written out for us, while some are mostly blank. Let’s focus on the model first, by adding some validation rules:
# app/models/post.rb class Post < ApplicationRecord validates :title, :body, presence: true validates :title, length: { within: 5..50 } end
We just added some basic validation rules to our Post model. These make sure a post always has a title and a body, while also limiting the title’s length to be at least 5 characters and less than 50 characters (this is totally arbitrary and only for illustration purposes). Now, edit the model’s unit test file so it looks like this:
require 'rails_helper' RSpec.describe Post, type: :model do context 'validations' do it 'accepts a valid post' do post = Post.new(title: 'A decent title', body: 'Some body') expect(post.save).to be(true) end it 'validates presence of title' do post = Post.new(body: 'Some body') expect(post.save).to be(false) end it 'validates length of title' do post = Post.new(title: 'A' * 51, body: 'Some body') expect(post.save).to be(false) end it 'validates presence of body' do post = Post.new(title: 'A decent title') expect(post.save).to be(false) end end end
We’ve simply added some examples to test all validations. We first test we can actually save a correct model, then we test all validation rules to make sure they’re actually validating. The context
keyword allows you to group specs depending on what they test. In this case, we’re testing validation rules, so it makes sense to group them. Go ahead and run these tests by running bundle exec rspec spec/models
in your terminal:
.... Finished in 0.02816 seconds (files took 1.32 seconds to load) 4 examples, 0 failures
Each little dot shows a successful test. Did you notice something missing in our tests? Remember that we specified a title should be at least 5 characters, but we don’t have any example testing that. Let’s add that:
it 'validates a title is not too short' do post = Post.new(title: 'Hi', body: 'Some body') expect(post.save).to be(false) end
And run your tests again. You should still see success all around. What happens when a test fails though? Let’s try that. Modify your model’s validations so the length reads within: 0..50
. This should mean the test we just added will fail. Try it:
...F. Failures: 1) Post validations validates a title is not too short Failure/Error: expect(post.save).to be(false) expected false got true # ./spec/models/post_spec.rb:26:in `block (3 levels) in <top (required)>' Finished in 0.0588 seconds (files took 1.24 seconds to load) 5 examples, 1 failure Failed examples: rspec ./spec/models/post_spec.rb:23 # Post validations validates a title is not too short
The example fails, as expected.
What we just did is write unit tests. These test single units of code (in this case validations). Any methods you might later add to this model would also need to be tested here to make sure they behave as expected. For example, if we add a slug
method, which turns the title into a more URL-friendly string, could look like this:
def slug title.parameterize end
And the corresponding test:
describe '#slug' do it 'converts to url-friendly string' do post = Post.new(title: 'This is a whacky string & text', body: 'body') expect(post.slug).to eq('this-is-a-whacky-string-text') end end
A couple of things to note here. First, we’re using describe '#slug'
to write our examples. This is a common practice, where we “describe” what the #slug
method does, or how it behaves. Second, we’re not writing many examples, because our method delegates the conversion of our title to a slug to Rails’ parameterize
method, which we can assume is thoroughly tested in Rails itself. In case we had written our own logic, it would make a lot of sense to include a bunch of extra examples and edge-cases.
Step 3: Other tests
Rails generated a bunch of other tests when we generated the scaffold for our posts. Let’s go over them to see what they’re testing, and adapt the ones that need some changes. The first one Rails generates is one for routing. You can consider this unit testing for your routes.rb
file, where we make sure specific paths get routed to the controller and method we want, passing the parameters we expect. Let’s take a look at the posts_routing_spec.rb
file. The examples should be pretty straightforward, where we expect a certain path to be routed to a certain method:
it "routes to #update via PATCH" do expect(patch: "/posts/1").to route_to("posts#update", id: "1") end
This tests that a PATCH
request to /posts/1
is routed to the Posts controller’s update
method, with the parameter id
set to 1
. More complex routing might require more intricate examples.
Another batch of tests Rails generated for us is the “requests” test for our Post scaffold. You can consider these integration specs since they test the results of API requests. In these, we’re really testing what happens from the moment we make a request, to the response and changes in the database that are triggered by it. Check out posts_spec.rb
. You’ll notice the file is pretty well described in the header, and that we’re supposed to fill out some blanks in order to make it work. Let’s wait on that a bit. First, take a look at the examples and what they test:
describe "GET /show" do it "renders a successful response" do post = Post.create! valid_attributes get post_url(post), as: :json expect(response).to be_successful end end
This, albeit a very simplified example, first creates a post in the database with valid attributes and then tests that GET
ing that post doesn’t yield an error. Ideally, we should add an extra example making sure the actual JSON response returns what we expect. We’ll get to that in a bit. Check out this other example in the same file:
describe "PATCH /update" do context "with valid parameters" do let(:new_attributes) { skip("Add a hash of attributes valid for your model") } it "updates the requested post" do post = Post.create! valid_attributes patch post_url(post), params: { post: invalid_attributes }, headers: valid_headers, as: :json post.reload skip("Add assertions for updated state") end it "renders a JSON response with the post" do post = Post.create! valid_attributes patch post_url(post), params: { post: invalid_attributes }, headers: valid_headers, as: :json expect(response).to have_http_status(:ok) expect(response.content_type).to eq("application/json") end end context "with invalid parameters" do it "renders a JSON response with errors for the post" do post = Post.create! valid_attributes patch post_url(post), params: { post: invalid_attributes }, headers: valid_headers, as: :json expect(response).to have_http_status(:unprocessable_entity) expect(response.content_type).to eq("application/json") end end end
This tests updates to a Post. Notice there are two context
s: one for valid parameters and one for invalid. You’re not necessarily supposed to write tests like this, but it’s a good idea since it helps organize the file. Also, notice how the examples are not complete: we’re supposed to add some new attributes and assertions. Let’s go over these tasks now so the examples work. First, let’s update the example attributes wherever there’s a “skip” method call. There’s three of those:
let(:valid_attributes) { { title: 'Some valid title', body: 'Lorem ipsum.' } } let(:invalid_attributes) { # Too short title { title: 'X' } } # ... let(:new_attributes) { { title: 'An edited title', body: 'An edited body' } }
Now, if you read those examples carefully, you might notice that, at least at the time of writing this article, there’s a bug in the generated examples, wherein the PATCH
tests they use invalid_attributes
instead of the new_attributes
. You might not bump into this. Make sure you fix that, otherwise your tests will never succeed! You’ll also notice if you run the specs (using bundle exec rspec spec/requests
), that some expectations fail:
1) /posts POST /create with invalid parameters renders a JSON response with errors for the new post Failure/Error: expect(response.content_type).to eq("application/json") expected: "application/json" got: "application/json; charset=utf-8" (compared using ==)
You should replace those expectations with something like this:
expect(response.content_type).to match(a_string_including("application/json"))
We should also add some expectations to the PATCH
examples, to make sure editions work:
it "updates the requested post" do post = Post.create! valid_attributes patch post_url(post), params: { post: new_attributes }, headers: valid_headers, as: :json post.reload expect(post.title).to eq('An edited title') expect(post.body).to eq('An edited body') end
Noticed the two last expectations. They test that the endpoint actually edits the post. We should also show you how you can test the JSON response of your API, by modifying the example for the GET /posts/1
endpoint, as we mentioned before:
describe "GET /show" do it "renders a successful response" do post = Post.create! valid_attributes get post_url(post), as: :json expect(response).to be_successful json = JSON.parse(response.body) expect(json['title']).to eq('Some valid title') expect(json['body']).to eq('Lorem ipsum.') end end
If you run the specs now, they should all pass. Otherwise, check out the errors and try to understand what’s going on. Remember that the generated specs are only a template, and you should build on top of this for proper test coverage.
RapidAPI Testing
Like we mentioned at the beginning of the article, there are many ways of testing APIs, and many purposes for each one. RapidAPI provides a new service called RapidAPI Testing, which enables you to run something similar to the request tests we just covered. One of the advantages of using RapidAPI Testing is that it enables you to monitor your API by running the tests from different geographical locations, giving you great insight into your API’s performance and stability.
Conclusion
We hope this article gave you a good starting point on how testing works, and how you can leverage it for your Ruby and Ruby on Rails APIs. Testing of applications is something we’d strongly encourage, since the time invested in writing a good test suite far outweighs the time lost tracking down bugs, changes, and regressions from the lack of proper testing.
Leave a Reply