Intro to RSpec in Rails (part 1): Basic Syntax and Strategy for Testing
Tags: rails, rspec, ruby, tutorial
Published: 2019-12-01 17:00:46

---
title: "Intro to RSpec in Rails (part 1): Basic Syntax and Strategy for Testing"
published: true
description: an introduction to common syntax and testing strategy for using rspec in rails
tags: rails, rspec, ruby, tutorial
series: "Intro to RSpec in Rails"
---
While working on [the character-generator API from my last post](https://dev.to/isalevine/pros-and-cons-of-ruby-s-tap-method-2m3j), I found myself writing most of my code in the `RandomCharacterGenerator` service object. As the class got more complicated, I realized that I had a very clear idea of the expected behavior--**so this was a perfect opportunity to practice writing RSpec tests for those behaviors!**

RSpec is a behavior-driven development (BDD) testing tool for Ruby, and is widely used for testing both plain ol' Ruby and full-on Rails applications. I'm writing this as an intro to some basic patterns and syntax used in RSpec Rails testing, as well as some basic BDD strategy for breaking down code into testable units.


## Behavior-Driven Development (BDD)
It's hard to understand RSpec without having some idea about behavior-driven development (BDD), so let's start there!

BDD is a testing philosophy that grew out of test-driven development (TDD). Essentially, the goal of both is: **write tests first, _then_ write code to make the tests pass!**

The idea behind this is that being able to identify and articulate the behaviors you want to test *should help guide you when writing the code itself*: 
* If you know what behavior you need to see, you can write tests for it
* If you write tests for it, you will know when your code is doing that behavior correctly
* If you know when your code is doing that behavior correctly, you can:
	* write as little code as possible (to get the test to pass)
	* refactor with the confidence that the tests will tell you if the behavior is not working anymore

Applying this to Rails (or Ruby more broadly, or object-oriented programming languages in general), these are the main steps we will use:
**1. Decide on the class/method/behavior to test**
**2. Write tests for that class/method/behavior**
**3. Write code to make the tests pass**
**4. When refactoring your code, make sure the tests still pass**


# RSpec
## Adding RSpec to a new or existing Rails app

[This is an excellent StackOverflow discussion for both starting a new Rails app with RSpec, and migrating an existing Rails app to RSpec (from `test-unit`, the default Rails test tool)](https://stackoverflow.com/a/6729210). The following instructions are pulled from the thread's answers.

### Starting a new Rails app with no test tool, and adding RSpec 

[Per user Zabba](https://stackoverflow.com/a/6728831):

>**at command line:**
```
rails new MYAPP -T # The -T option tells rails not to include Test::Unit
```
>**in Gemfile:**
```
gem 'rspec-rails'
```
>**at command line:**
```
bundle install
rails g rspec:install
```

### Migrate an existing Rails app with default `test-unit` to RSpec
[Per user Sayuj](https://stackoverflow.com/a/6729210): 
**NOTE: This does include deleting all existing files in `test-unit`'s directory! Make sure to back up or migrate any existing tests before following these steps!!**
>Create your new rails application as:
```
rails new <app_name> -T
```
>Or remove your  `test`  directory from your existing application:
```
rm -rf test/
```
>Make an entry in your Gemfile:
```
gem 'rspec-rails'
```
>From the command line install the gem
```
$ bundle install
```
>From the command line install rspec into your application:
```
$ rails g rspec:install
```
>Now your rails application uses RSpec instead of test-unit.

## Running RSpec tests
To run all RSpec tests in your Rails project, use this console command:
```
bundle exec rspec
```
[Per StackOverflow user apneadiving](https://stackoverflow.com/a/6116744), you can also run individual tests by specifying the filepath and line number:
```
bundle exec rspec ./spec/controllers/groups_controller_spec.rb:42
```


## RSpec building blocks: `describe` and `it`
In RSpec, the main building blocks for test suites are `describe` (for grouping) and `it` (for tests). We write both of them as `do...end` blocks, where we do things like instantiate classes, set variables, and write individual tests.

Here's the first test from `random_character_generator_spec.rb`:
```ruby
# /spec/services/random_character_generator_spec.rb

RSpec.describe RandomCharacterGenerator do
    describe "#new_character" do
        # NOTE: Do NOT create your test variables this way!! (See comments for why.) This is just an example for readability...
        rcg = RandomCharacterGenerator.new
        player = Player.create(user_name: "Ronald McDonald", display_name: "Mac")
        character = rcg.new_character("Ronnie the Rat", player)

        it "creates a new Character instance" do
            expect(character).to be_an_instance_of Character
        end

    end
end
```

Let's walk through this:
1. At the top, we have `RSpec.describe <ClassName> do`. This encapsulates our tests for the `RandomCharacterGenerator` service object class.
2. Underneath, we have `describe "#method_name" do`. This encapsulates our tests for the `new_character` method. It is a Rails convention to add `#` before this--that way, the test output will read `RandomCharacterGenerator#new_character`, which helps us know what we're testing.
3. Inside that `describe` block for our method, we instantiate a couple objects to test, and run the `new_character` method so we can assign its output to the variable `character`. This will give us all the objects we need to test the method's behavior. 
(**NOTE: This is NOT the right way to instantiate variables in an RSpec test! It is here only for an introductory example. See [this excellent comment from Andrew Brown](https://dev.to/andrewbrown/comment/id4h) [and [the next post in this series](https://dev.to/isalevine/intro-to-rspec-in-rails-part-2-improving-tests-with-let-and-context-241n)] to see why and learn the proper alternatives!**)
4. Finally, we have an `it "description of expected behavior" do` block. This is where we write the test code logic! In a very expressive style, we see that the test itself **expects the variable `character` to be an instance of the class `Character`**. If this line evaluates to True, our test will pass--otherwise, it will fail!

## Setting clear `expect`ations
As we saw above, RSpec provides an expressive domain-specific language (DSL) to write our tests with. The idea here is that methods can be chained together in a way that sounds very close to plain English. The code `expect(character).to be_an_instance_of Character` almost reads like a real sentence!

**The most important syntax is `expect(foo).to`, because this creates the test itself!** It's also important because _we must know what **one specific thing** we're testing_ (foo) in order to write the test.

### Here are some common syntax options, [all sourced from the excellent resource RSpec Cheatsheet by @rstacruz](https://devhints.io/rspec):

**Equal value (or not)**
```rb
expect(target).to eq 1
expect(target).not_to eq 1
```
**Math comparisons**
```rb
expect(5).to be < 6
expect(5).to == 5
expect(5).to equal value
expect(5).to be_between(1, 10)
expect(5).to be_within(0.05).of value
```
**Type-specific**
```rb
expect(x).to be_zero    # FixNum#zero?
expect(x).to be_empty   # Array#empty?
expect(x).to have_key   # Hash#has_key?
```
**Objects**
```rb
expect(obj).to be_an_instance_of MyClass
expect(obj).to be_a_kind_of MyClass
```
**Errors**
```rb
expect { user.save! }.to raise_error
expect { user.save! }.to raise_error(ExceptionName, /msg/)
```
**Enumerables**
```rb
expect(list).to include(<object>)

expect(list).to have(1).things
expect(list).to have_at_least(2).things
expect(list).to have_at_most(3).things
```

## Common RSpec strategy: (1) pick a class, (2) pick a method, (3) outline expected behaviors, (4) write one `it` block per behavior
In the example test above, our three layers of `describe`/`describe`/`it` blocks correspond to increasingly specific things we're testing:
* At the highest level, we have the class `RandomCharacterGenerator`
* Within that class, we have the method `new_character`
* Within that method, we have the expected behavior "creates a new Character instance"

This maps well to the overall BDD strategy we outlined earlier. With RSpec specifically, we can translate this into four steps:
**1. Pick a class to test**
**2. Pick a method on the class to test**
**3. Outline expected behaviors from that method _(input/output, object/state mutations, errors, etc.)_**
**4. Write one `it` block per behavior. This block can contain ONE or MANY `expect(foo).to` tests _(to cover specific values, test/edge cases, etc.)_**

Rails makes picking classes straightforward--choose a model, a controller, a service object, anything defined as a class!

Ideally, you'll want to test the functionality of individual methods, as well as an overall input/output from a chain of methods. Doing both will help give you confidence that you know what each step of your code is doing (and provide a safety net for catching errors when refactoring).

**In BDD, it is very important to think of expected behaviors AHEAD OF TIME!** This is good practice to reinforce approaching your code with a deliberate plan. Be able to break down your methods (and overall functionality) into individual behaviors, as well as test cases (both expected and edge cases).


# Use case: character-generator API

## Here's a review of [the character-generator API from my last post](https://dev.to/isalevine/pros-and-cons-of-ruby-s-tap-method-2m3j) (with some recent refactors):

Our schema includes a `Character` model, which belongs to a `Player` model. Characters have a name string and four stats as integers (strength, dexterity, intelligence, charisma), and a foreign key for their Player. Players have a user name and a display name, both strings.
```ruby
# /db/schema.rb

ActiveRecord::Schema.define(version: 2019_11_24_071655) do

  create_table "characters", force: :cascade do |t|
    t.string "name"
    t.integer "strength"
    t.integer "dexterity"
    t.integer "intelligence"
    t.integer "charisma"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
    t.integer "player_id", null: false
    t.index ["player_id"], name: "index_characters_on_player_id"
  end

  create_table "players", force: :cascade do |t|
    t.string "user_name"
    t.string "display_name"
    t.datetime "created_at", precision: 6, null: false
    t.datetime "updated_at", precision: 6, null: false
  end

  add_foreign_key "characters", "players"
end
```

The `RandomCharacterGenerator` service object is a class that handles all of the logic to generate a new Character, and assign their stats points randomly based on the available `@points_pool` and `@max_roll` attributes. There's also an attribute for `@stats_array` that contains strings for the four Character attributes. (Yes, I know that was lazy of me...)

The public method `new_character` returns a new Character instance with its stats filled in. There is also a private method, `roll_stats`, that handles the stat-randomizing:

```ruby
# /app/services/random_character_generator.rb

class RandomCharacterGenerator
    attr_accessor :stats_array, :points_pool, :max_roll
    
    def initialize
        @stats_array = ["strength", "dexterity", "intelligence", "charisma"]
        @points_pool = 9
        @max_roll = 6
    end


    def new_character(name, player)
        # more code needed here to make tests pass
    end


    private

    def roll_stats(character, stats_array, points_pool, max_roll)
        # more code needed here to make tests pass
    end

end
```

For this article, we'll group all our tests together by the method `new_character`, as we're concerned with the output Character it returns. Because this method relies on the private method `roll_stats`, we *are* indirectly testing that too. However, some of these tests *could* be rewritten to be directly attached to `roll_stats` as well.


## Let's go through the RSpec strategy steps above:

**1. Pick a class:** 
 * `RandomCharacterGenerator` service object

**2. Pick a method on the class:**
*  `new_character`

**3. Outline expected behaviors:**
* It creates a new Character instance
* It randomly allocates all stat points (9) between four Character stats (strength, dexterity, intelligence, charisma)
*  It allocates stat points so stat values are between 1 and max roll (6)
* It saves the Character to the database

**4. Write one `it` block per behavior:**

```ruby
# /spec/services/random_character_generator_spec.rb

RSpec.describe RandomCharacterGenerator do

    describe "#new_character" do
        # NOTE: Do NOT create your test variables this way!! (See comments for why.) This is just an example for readability...
        rcg = RandomCharacterGenerator.new
        player = Player.create(user_name: "Ronald McDonald", display_name: "Mac")
        character = rcg.new_character("Ronnie the Rat", player)

        it "creates a new Character instance" do
	        # expect(x).to
        end

        it "randomly allocates all #{rcg.points_pool} stat points between #{rcg.stats_array.to_s}" do
            # expect(x).to
        end

        it "allocates stat points so stat values are between 1 and max roll (#{rcg.max_roll})" do
            # expect(x).to
        end

        it "saves the Character to the database" do
            # expect(x).to
        end
    end
end
```

## Creating our tests

Let's see our tests by running `bundle exec rspec`. This will execute all the tests available, and return the results to our console:
```
$ bundle exec rspec
....

Finished in 0.01019 seconds (files took 0.8859 seconds to load)
4 examples, 0 failures
```
Our tests are technically passing, because we haven't filled them in yet! Let's build them one-by-one.

### 1. `it "creates a new Character instance"`
We can test whether a variable is an instance of a particular class with `be_an_instance_of`:
```ruby
# /spec/services/random_character_generator_spec.rb

	it "creates a new Character instance" do
        expect(character).to be_an_instance_of Character
    end
```
Run `bundle exec rspec`:
```
Failures:

  1) RandomCharacterGenerator#new_character creates a new Character instance
     Failure/Error: expect(character).to be_an_instance_of Character
       expected nil to be an instance of Character
     # ./spec/services/random_character_generator_spec.rb:70:in `block (3 levels) in <top (required)>'

Finished in 0.03433 seconds (files took 0.91909 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:69 # RandomCharacterGenerator#new_character creates a new Character instance
```
Great! Now we're ready to fill in the code in `new_character` to make this test pass. **This is the heart of BDD (and TDD): write the tests first, then write the code!**
```ruby
# /app/services/random_character_generator.rb

	def new_character(name, player)
        Character.new(name: name, player: player).tap do |character|
	        # roll_stats() will be called here
            # we will save! the character here
        end
    end
```
This will make our tests pass (even though `roll_stats` doesn't do anything yet). 1 of 4 complete!


### 2. `it "randomly allocates all 9 stat points between (strength, dexterity, intelligence, charisma)"`
Since we have the stats we want to check stored in `@stats_array`, we can use `reduce` to iterate through it, access `character[stat]` to check its value, and add up the integers for each stat. We can then use that with `expect(x).to eq value` to check for equality:
```ruby
# /spec/services/random_character_generator_spec.rb

	it "randomly allocates all #{rcg.points_pool} stat points between #{rcg.stats_array.to_s}" do
        expect(rcg.stats_array.reduce(0) { |points, stat| points += character[stat] }).to eq rcg.points_pool 
    end
```
Let's run `bundle exec rspec` to see what the failing test outputs:
```
Failures:

  1) RandomCharacterGenerator#new_character randomly allocates all 9 stat points between ["strength", "dexterity", "intelligence", "charisma"]
     Failure/Error: expect(rcg.stats_array.reduce(0) { |points, stat| points += character[stat] }).to eq rcg.points_pool
     
     TypeError:
       nil can't be coerced into Integer
     # ./spec/services/random_character_generator_spec.rb:74:in `+'
     # ./spec/services/random_character_generator_spec.rb:74:in `block (4 levels) in <top (required)>'
     # ./spec/services/random_character_generator_spec.rb:74:in `each'
     # ./spec/services/random_character_generator_spec.rb:74:in `reduce'
     # ./spec/services/random_character_generator_spec.rb:74:in `block (3 levels) in <top (required)>'

Finished in 0.01359 seconds (files took 0.86408 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:73 # RandomCharacterGenerator#new_character randomly allocates all 9 stat points between ["strength", "dexterity", "intelligence", "charisma"]
```
Since nothing is assigned to each `character[stat]` yet, we're getting an error saying nil cannot be coerced into an integer for addition. Let's add some code to the `roll_stats` method, and call it inside `new_character`:
```ruby
# /app/services/random_character_generator.rb

	def new_character(name, player)
		Character.new(name: name, player: player).tap do |character|
            roll_stats(character, @stats_array, @points_pool, @max_roll)
            # we will save! the character here
        end
	end


	private

	def roll_stats(character, stats_array, points_pool, max_roll)
        stats_array.each do |stat|
            roll = rand(1..10)
            character[stat] = roll
            points_pool -= roll
        end
	end
```
Run `bundle exec rspec`:
```
Failures:

  1) RandomCharacterGenerator#new_character randomly allocates all 9 stat points between ["strength", "dexterity", "intelligence", "charisma"]
     Failure/Error: expect(rcg.stats_array.reduce(0) { |points, stat| points += character[stat] }).to eq rcg.points_pool
     
       expected: 9
            got: 20
     
       (compared using ==)
     # ./spec/services/random_character_generator_spec.rb:74:in `block (3 levels) in <top (required)>'

Finished in 0.02692 seconds (files took 0.84623 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:73 # RandomCharacterGenerator#new_character randomly allocates all 9 stat points between ["strength", "dexterity", "intelligence", "charisma"]
```
Great, we've got a new error! The value we're adding up with `reduce` isn't matching the expected value (9). Let's add a bit more code to ensure that `@points_pool` isn't exceeded by each `rand(1..max_roll)`:
```ruby
# /app/services/random_character_generator.rb

	def roll_stats(character, stats_array, points_pool, max_roll)
        stats_array.each do |stat|
            roll = rand(1..10)

            if roll > points_pool
                roll = points_pool
            end
            
            character[stat] = roll
            points_pool -= roll
        end
    end
```
Awesome, our test will now pass!

### 3. `it "allocates stat points so they do not exceed max roll 6"`
For this, let's write four separate `expect` tests in the same `it` block. Each one will test an individual `character[stat]`, and the test will only pass if **all four `expects` are true**:
```ruby
# /spec/services/random_character_generator_spec.rb

        it "allocates stat points so stat values are between 1 and max roll (#{rcg.max_roll})" do
            expect(character.strength).to be_between(1, rcg.max_roll)
            expect(character.dexterity).to be_between(1, rcg.max_roll)
            expect(character.intelligence).to be_between(1, rcg.max_roll)
            expect(character.charisma).to be_between(1, rcg.max_roll)
        end
```
Let's check out our result with `bundle exec rspec`:
```
Failures:

  1) RandomCharacterGenerator#new_character allocates stat points so stat values are between 1 and max roll (6)
     Failure/Error: expect(character.strength).to be_between(1, rcg.max_roll)
       expected 8 to be between 1 and 6 (inclusive)
     # ./spec/services/random_character_generator_spec.rb:78:in `block (3 levels) in <top (required)>'

Finished in 0.04061 seconds (files took 0.91006 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:77 # RandomCharacterGenerator#new_character allocates stat points so stat values are between 1 and max roll (6)
```
Okay, so our rolls are going too high! (However, it IS possible for the stats to randomly all be in the correct range--**beware of false positives!**) 

Change `roll_stats` so that our `rand()` call cannot exceed `@max_roll`:
```ruby
# /app/services/random_character_generator.rb

	def roll_stats(character, stats_array, points_pool, max_roll)
        stats_array.each do |stat|
            roll = rand(1..max_roll)

            if roll > points_pool
                roll = points_pool
            end
            
            character[stat] = roll
            points_pool -= roll
        end
    end
```
Perfect! 

**Sidenote: the final code to allocate points across an _arbitrary_ number of possible stats has `roll_stats` written to be this (and debugged using `.tap`!), which is recognizable from the previous article:**
```ruby
# /app/services/random_character_generator.rb

	def roll_stats(character, stats_array, points_pool, max_roll)
        stats_array.each_with_index do |stat, index|
            roll = rand(1..max_roll)                                    .tap {|r| puts "roll: #{r}"}
            remaining_stats = ((stats_array.length - 1) - index)        .tap {|r| puts "remaining_stats: #{r}"}
                                                                        .tap {|r| puts "points_pool (before): #{points_pool}"}
            if remaining_stats == 0
                character[stat] = points_pool
                points_pool = 0
            elsif points_pool - roll < remaining_stats
                max_points = points_pool - remaining_stats
                character[stat] = max_points
                points_pool -= max_points
            else
                character[stat] = roll
                points_pool -= roll
            end                                                         .tap {|r| puts "character[#{stat}]: #{character[stat]}"}
                                                                        .tap {|r| puts "points_pool (after): #{points_pool}\n\n"}
        end
    end
```
Run the tests again, and they will still pass!

### 4. `it "saves the Character to the database"`
We'll check if our Character is successfully saved to the database by checking if the number of Character records stored in the database has increased by 1.

To start, let's get a `starting_database_count` variable set up in our `describe "#new_character` block. We'll use the ActiveRecord `.count` method on the `Character` model to get this number:
```ruby
# /spec/services/random_character_generator_spec.rb

RSpec.describe RandomCharacterGenerator do

    describe "#new_character" do
        # NOTE: Do NOT create your test variables this way!! (See comments for why.) This is just an example for readability...
        starting_database_count = Character.count

        rcg = RandomCharacterGenerator.new
        player = Player.create(user_name: "Ronald McDonald", display_name: "Mac")
        character = rcg.new_character("Ronnie the Rat", player)

		...

        it "saves the Character to the database" do
            # expect(x).to
        end
    end
end
```

Now, we can use `expect(x).to eq value` to check if the `Character.count` value *after* calling `new_character` has increased by 1:
```ruby
# /spec/services/random_character_generator_spec.rb

    it "saves the Character to the database" do
        expect(Character.count).to eq (starting_database_count + 1)
    end
```
Let's see what the failing test outputs with `bundle exec rspec`:
```
Failures:

  1) RandomCharacterGenerator#new_character saves the Character to the database
     Failure/Error: expect(Character.count).to eq (starting_database_count + 1)
     
       expected: 1736
            got: 1735
     
       (compared using ==)
     # ./spec/services/random_character_generator_spec.rb:87:in `block (3 levels) in <top (required)>'

Finished in 0.02612 seconds (files took 0.88349 seconds to load)
4 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:86 # RandomCharacterGenerator#new_character saves the Character to the database
```
Okay, so our Character isn't being saved. Let's toss a `save!` call in the `.tap` block called on `Character.new`:
```ruby
# /app/services/random_character_generator.rb

	def new_character(name, player)
        Character.new(name: name, player: player).tap do |character|
            roll_stats(character, @stats_array, @points_pool, @max_roll)
            character.save!
        end
    end
```

And our final test output:
```
$ bundle exec rspec
....

Finished in 0.01343 seconds (files took 0.86781 seconds to load)
4 examples, 0 failures
```
Hooray! We now have test coverage for our `RandomCharacterGenerator#new_character` method!


# Conclusion
RSpec is a great tool to help you familiarize yourself with behavior-driven development and testing in Rails. 

I highly recommend reading through the resources below, especially the top few (including the RubyGuides tutorial, an excellent intro tutorial by fellow Flatiron alum Aliya Lewis, and the Devhints RSpec cheatsheet!).

## Further reading/links/resources about RSpec testing:
* [https://www.rubyguides.com/2018/07/rspec-tutorial/](https://www.rubyguides.com/2018/07/rspec-tutorial/)
* [https://dev.to/aliyalewis/testing-the-waters-with-rspec-23ga](https://dev.to/aliyalewis/testing-the-waters-with-rspec-23ga)
* [https://dev.to/aliyalewis/testing-the-waters-with-rspec-part-2-30po](https://dev.to/aliyalewis/testing-the-waters-with-rspec-part-2-30po)
* [https://devhints.io/rspec](https://devhints.io/rspec)
* [https://stackoverflow.com/questions/6728618/how-can-i-tell-rails-to-use-rspec-instead-of-test-unit-when-creating-a-new-rails/6729210#6729210](https://stackoverflow.com/questions/6728618/how-can-i-tell-rails-to-use-rspec-instead-of-test-unit-when-creating-a-new-rails/6729210#6729210)
* [https://stackoverflow.com/questions/6116668/how-to-run-a-single-rspec-test](https://stackoverflow.com/questions/6116668/how-to-run-a-single-rspec-test)
* [https://en.wikipedia.org/wiki/Behavior-driven_development](https://en.wikipedia.org/wiki/Behavior-driven_development)

### Got any tips, tricks, or instances for testing with RSpec? Please feel free to comment and share below! <3