If you’re a developer, then you've already performed a software test, whether you realize it or not. When you write a piece of software, to see if it's working, that's testing.
When you create a webform, then submit the form to see if it successfully adds the data to the database; that’s testing. When you submit the same form again, with the wrong input, to see if it rejects the form and tells the user there’s a problem, that’s also testing but manual testing.
Computers are great at performing repetitive tasks so when we talk about testing, we’re referring to code that can perform tests on other codes. RSpec is a framework that allows us to do that. The ‘R’ stands for Ruby and ‘Spec’ stands for Specification (detailed requirements that our code should meet). It’s often referred to as, “An executable example that tests whether a portion of code exhibits the expected behavior in a controlled context.”
In software testing, RSpec has a basic structure as follows;
‘Given, When, Then’ is the basic structure of all tests.
For example, “Given a web form → When I fill out the form and submit, → Then I expect the data to add to the database successfully.”
Now, you may ask why software tests are important? Honestly, it would be difficult to have a career as a professional developer if you don’t write software tests.
As counter-intuitive as it sounds, it’s also essential to look at the reasons why you should not write software tests in order to weigh them against reasons we should write tests.
Now the question is, should we always test?
Small applications may not need testing, but testing becomes more critical as the complexity increases.
You don’t need Rails to use RSpec as RSpec will test any Ruby code. That said, Rails has made RSpec popular.
In this article, we’re going to take an in-depth look at how you can use RSpec with Ruby on Rails. RSpec with Ruby on Rails requires a different Ruby Gem, rspec-rails. For details and documentation on this, check out the rspec-rails GitHub page.
Including this gem in your application adds:
You should also note that the documentation page is a living document, therefore it’s possible changes may occur at the time you’re reading this article, so you should refer back to the documentation page from time to time. That said, the basics of the process should stay the same.
Add Rspec-Rails to both the :development and :test groups of your app’s Gemfile.
Note: adding it to the :development group is not strictly necessary, but without it, generators and rake tasks must be preceded by RAILS_ENV=test.
Next, make sure you are in your project directory;
{% code-block language="js" %}
# Download and install
$ bundle install
# Generate boilerplate configuration files
# (check the comments in each generated file for more information)
$ rails generate rspec:install
create .rspec
create spec
create spec/spec_helper.rb
create spec/rails_helper.rb
{% code-block-end %}
You will notice that Rails creates four files for you. To avoid confusion, you’ll have to choose one. It’s perfectly okay to delete the ‘test’ directory since we will be using RSpec for our testing.
You don’t need to worry too much about the difference between the rails_helper.rb and spec_helper.rb files as the difference is a bit of a technical point.
spec_helper.rb — things specific to RSpec and that do not require the Rails environment should be here.
rails_helper.rb — things that are about Rails or require Rails dependency should be here.
If you load rails_helpers, it loads spec_helpers at the same time.
Rails generators will generate placeholder files for your RSpec test. The standard rails generators will create appropriate spec files for you, as long as you have rspec-rails gem installed.
RSpec knows how to generate a number of different placeholder files. Below are the most frequently used generators:
{% code-block language="js" %}
$ rails generate model user
{% code-block-end %}
The line of code above will also generate spec/models/user_spec.rb.
If you need to generate the spec file manually - perhaps because you are working on an existing application and model file has already been generated - the process is almost identical, you simply put rspec: in front of the type you want to create.
{% code-block language="js" %}
$ rails generate rspec:model user
{% code-block-end %}
Let’s take a look inside the generated file. The first thing it does is require the rails_helper file. Remember, the rails_helper in-turn requires the spec_helper file.
{% code-block language="js" %}
require 'rails_helper'
RSpec.describe User, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end
{% code-block-end %}
`RSpec.describe` is just technically ensuring it’s scoped by RSpec, followed by a class name ‘User’ which allows it to implicitly set the subject for RSpec. The type allows RSpec to recognize what type of test this is. The ‘pending’ as the name implies it is a pending example allowing you to test run your specs.
Now that you have finished initializing your project to run with RSpec, the next thing is to ensure it actually does run.You’re likely already familiar with typing ‘rspec’ into your command prompt to fire it up. Don’t worry, that still works here in Rails.
First, let's look at the documentations way of running it:
{% code-block language="js" %}
# Default: Run all spec files (i.e., those matching spec/**/*_spec.rb)
$ bundle exec rspec
# Run all spec files in a single directory (recursively)
$ bundle exec rspec spec/model
# Run a single spec file
$ bundle exec rspec spec/controllers/accounts_controller_spec.rb
# Run a single example from a spec file (by line number)
$ bundle exec rspec spec/controllers/accounts_controller_spec.rb:8
# See all options for running specs
$ bundle exec rspec --help
{% code-block-end %}
Now, you’ve seen two methods in running specs, so let’s put it all together:
The first is more light-weight and I tend to use it more often. That said, ‘bundle exec rspec’ makes sure that all gems and things needed to run Rspec are loaded - it’s a kind of prefix to make sure RSpec runs in the right environment.
The last two, ‘rake spec’ and ‘rake’ require a little more setup. Sometimes you need to make sure the database setup is sufficient for you to run RSpec.
One of the most noticeable differences about working with RSpec on a basic Ruby project versus Ruby on Rails is the use of databases.
The default rails application provides you with three environments to work with;
If you are new to testing, you may only be familiar with the first two. The idea behind all three is the same though, we can have unique configurations for development, production, and testing, yet keep them entirely separate.
For now, let’s focus on the database. You have three database configurations that correspond to each of these. That’s because you don’t want your development data to be mixed with your production data, nor your test data. We want to keep our test data independent from production, as well as development.
For example, if I’m developing a new feature and I have my database in a particular state then stop to run my test, I don’t want my test to stop what I have in development.
Therefore, the test database has its own database it can work with. I can read to it, write to it, and completely clear out its entire contents without affecting what's happening in development.
In order to use the test database, we need to prepare it to ensure it’s ready.
Remember, I mentioned earlier that running RSpec alone does not prepare the test database as the rake does. Although, from the word, go rails sets up your whole environment for you.
Calling rake spec or just rake will prepare your test database. Behind the scene, it calls `db:test:prepare`. To see what happens under the hood, add the trace option: rake spec --trace. The actual steps rake spec takes are;
You can actually run these steps yourself if you don’t want `rake` to do it. Every time we run rails db:migrate it updates the schema file as well.
Note: if we load the schema file, we would have the same database as the development, with the right tables and columns, just without data.
You should save data to the test database only when it's necessary, as making database calls in your examples adds significant time to our test suite. A lot of the time, it’s really not needed - you don’t really need to save a user to the database to check if it’s valid.
An unsaved instance can do the job just as well, so you could have product.build or product.new. Test doubles can sometimes do the job or stubs. If your test suit is running slow, this probably could be the reason.
Each example in your test suite runs in a transaction, meaning that when an example finishes, database changes are rolled back — anything you do will be undone.
Looking at our spec/rails_helper.rb file, the line of code below makes this possible.
{% code-block language="js" %}
RSpec.configure do |config|
...
config.use_transactional_fixtures = true
{% code-block-end %}
'config.use_transactional_examples = true' is an alias for fixtures. Personally, I think .._examples is clearer but at the moment the default uses fixtures.
To test out this idea, see the examples below.
{% code-block language="js" %}
RSpec.describe 'Posts', type: :request do
it 'has 0 post at the start' do
expect(posts.count).to eq(0)
end
it 'has 1 post at after #create' do
expect(posts.count).to eq(1)
end
it 'has 0 post when using transactions' do
expect(posts.count).to eq(0)
end
{% code-block-end %}
No posts should still be in the database after the second example because of the transactional fixtures.
Note: examples do not necessarily run in order — there is a configuration option for running them randomly or, we could run each in isolation by specifying a line number from the command line. Refer to the running specs section of this article for more on this. Examples need to be kept atomic so that they can stand alone.
It is important to note that;
Say we want to have four users in our database and work with those throughout our examples. It makes sense to leave them there to give us a performance boost. You do not want to leave them there after our example group is done though. Use after(:context) to revert the modified data.
{% code-block language="js" %}
...
before(:context)
@user = User.create(name: "Uduak Essien")
end
...
...
...
after(:context) { @user.destroy }
...
{% code-block-end %}
A good example of before(:example) would be:
{% code-block language="js" %}
describe 'Authenticated Users Activities' do
before do
@user = User.create(name: 'User1', email: 'user1@gmail.com', password: 'password')
sign_in @user
end
it 'should successfully access post timeline' do
get posts_path
assert_response :success
end
it 'should be able to create a post' do
@post = @user.posts.new(content: 'test post').save
expect(@post).to eq(true)
end
end
{% code-block-end %}
Database-cleaner is another option if you don’t want to work with transactional fixtures. See the gem file for more on this.
By now, you should have an in-depth understanding of rails-specs. In the next article, we will go over how to work with rails specs in detail. I also want to give credit to Kevin Skoglund as a lot of the knowledge and insights in this article are from his tutorials on RSpec.
To learn more about Microverse, and joining our supportive community of remote software developers, get started below!
Career advice, the latest coding trends and languages, and insights on how to land a remote job in tech, straight to your inbox.