Experienced development teams understand the importance of TDD or Test Driven Development. If you're unfamiliar with the concept, don't fret. Here we will explain how writing tests can empower both engineers and organizations to build better software products.
Unit Tests are used to ensure that the code you write does what it is intended to do. The process looks like this; engineers make a list of requirements, write a Unit Test for a specific requirement and then modify the codebase to pass that test. As you build out your application, changes to the code may cause a test to fail. Your failed test will help you track down the error to the exact line of code and help you correct the issue faster.
For our clients, it allows their project budgets to stretch further. We aren’t wasting time sifting through thousands of lines of code wondering why a bug exists. Our tests alert us of issues, we fix them quickly and get back to building features.
Let's walk through an example of writing tests for posting a photo and caption in a Rails project.
User Stories
- As a user, I want to be able to submit a post with a photo and a caption
- As a user, I want to be able to view posts on my feed
Now that we have our requirements, let's write our tests. We will be using RSpec and FactoryBot. I recommend you check them out for your Rails app.Our first story clearly states that a post requires a photo and a caption. We can start with the model.
Post Model
- Should be invalid without photo
- Should be invalid without a captionWe first start with building a factory so that we have an instance of Post to test with.
# specs/factories.rb
FactoryBot.define do
factory :post do
photo create(:photo)
caption "This is a test caption"
user create(:user)
end
factory :user do
name "Admin"
end
factory :photo do
url "https://example.com/photo"
end
end
With the factory built, we can now test our Post model. We will start with some validation helpers. These helpers will allow us to write our validation tests easier.
# specs/support/helpers/validation_helpers.rb
require "rails_helper"
module ValidationHelpers
def build_and_validate(*args)
FactoryBot.build(*args).tap(&:validate)
end
def build_stubbed_and_validate(*args)
FactoryBot.build_stubbed(*args).tap(&:validate)
end
def create_and_validate(*args)
FactoryBot.create(*args).tap(&:validate)
end
end
RSpec.configure do |config|
config.include ValidationHelpers
end
And now our Post model tests. We are stating that the factory must be valid, that is that all of our validations pass. We also state that a post is invalid without a caption, photo, or user.
# specs/models/post_spec.rb
RSpec.describe Post, type: :model do
context "validation" do
it "has a valid factory" do
expect(create(:post)).to be_valid
end
it "is invalid without a user" do
post = build_stubbed_and_validate(:post, user: nil)
expect(post.errors).to have_key(:user_id)
end
it "is invalid without a caption" do
post = build_stubbed_and_validate(:post, token: nil)
expect(post.errors).to have_key(:caption)
end
it "is invalid without a photo" do
post = build_stubbed_and_validate(:post, photo: nil)
expect(post.errors).to have_key(:photo)
end
end
end
Running these tests will produce errors, such as, NameError: uninitialized constant Post
. This occurs because we obviously haven't written that code yet. So let's fix these errors! Let's assume that the User
and Photo
models already exist. We can start with creating the Post class.
# app/models/post.rb
class Post < ApplicationRecord
end
This fixes the NameError: uninitialized constant Post
error. Now we can go ahead and add the code to fix the validation errors from our tests. Here, we add our validations for a Post.
# app/models/post.rb
class Post < ApplicationRecord
validates :caption, presence: :true
validates :photo, presence: :true
validates :user, presence: :true
end
Now that everything for our model is built, we can run the tests again and the tests will pass!Now that we've built and tested our model, let's test our routes.
Post Routes Tests
-
POST /posts
should accept a photo and a captionStarting with posting, we test that with valid parameters, the post is created, otherwise an error is rendered.
# spec/requests/post_spec.rb
RSpec.describe "Posts", type: :request do
let(:post) { create(:post) }
let(:post_params) { attributes_for(:post) }
describe "POST /posts" do
before do
post "/posts", params: { post: post_params }
end
context "when given valid params" do
it "creates a new post" do
expect(response).to be_created
end
end
context "when given an invalid caption param" do
let(:post_params) { attributes_for(:post, caption: nil) }
it "renders a 422" do
expect(response.status).to eq(422)
end
end
context "when given an invalid photo param" do
let(:post_params) { attributes_for(:post, photo: nil) }
it "renders a 422" do
expect(response.status).to eq(422)
end
end
end
end
Now that our tests are written, let's write the code so that these will pass.
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def posts
post = Post.new(post_params)
if post.save
render json: post, status: :created
else
render json: post.errors, status: :unprocessable_entity
end
end
private
def post_params
params.require(:post).permit(:caption, :photo)
end
end
Running the full test suite will now show everything green. You're good to go!
The process allows developers to focus on writing efficient code and designing simple, yet highly effective architecture. In a future blog post, we’ll show you how to constantly run your suite of tests using continuous integration and CircleCI.
Need help bringing your mobile project to life? Our in-house team of designers and developers has built apps for over 70 clients. Get in touch with us.