Intro to RSpec in Rails (part 2): Improving Tests with `let` and `context` 
Tags: rails, rspec, ruby, tutorial
Published: 2019-12-08 19:40:22

---
title: "Intro to RSpec in Rails (part 2): Improving Tests with `let` and `context` "
published: true
description: improving rspec rails tests with the let and context methods
tags: rails, rspec, ruby, tutorial
series: "Intro to RSpec in Rails"
---

In our last post, we started to write some RSpec tests for our character-generator API, and its `RandomCharacterGenerator` service class.

[Dev.to community member Andrew Brown pointed out that several aspects of our code were not written optimally]([https://dev.to/andrewbrown/comment/id4h](https://dev.to/andrewbrown/comment/id4h)), so let's take a look at a few of his recommendations!


# Use `let` to wrap your testing variables
In our original test code, we simply created our testing variables inside our `describe` block. This is **not correct**--instead, we are expected to wrap this code in either a `before` or `let` block. Both of these will help RSpec understand _when_ to create the variables, and help our tests run correctly.

Here's the original test code we wrote (with a few omissions for readability):
```ruby
# /spec/services/random_character_generator_spec.rb

require 'rails_helper'

RSpec.describe RandomCharacterGenerator do
    
    describe "#new_character" do
        # NOTE: Do NOT create your test variables this way!!
        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

        # For now, we'll ignore the other tests we wrote...
    end
end
```
So, we need to do something about where we create those `starting_database_count`, `rcg`, `player`, and `character` variables!

## `before` or `let`
We have two options for wrapping our variable-creation: `before` or `let` blocks. 
* `before` is a hook that will run before each test (by default), and thus may be run multiple times when we don't need it to. 
* `let` is only called when a test needs the variable it creates.

So, we could rewrite our code two different ways:
```ruby
# /spec/services/random_character_generator_spec.rb

    describe "#new_character" do

		# OPTION 1 (run before each test):
	    before do
	        rcg = RandomCharacterGenerator.new
	        player = Player.create(user_name: "Ronald McDonald", display_name: "Mac")
	        character = rcg.new_character("Ronnie the Rat", player)
		end

		# OPTION 2 (run only when variables are called in a test):
		let(:player)	{ Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
	    let(:character) {
	        rcg = RandomCharacterGenerator.new
	        rcg.new_character("Ronnie the Rat", player)
	    }

	end
```

One resource Andrew pointed me to is [BetterSpecs](http://www.betterspecs.org/), an excellent list of guidelines for writing more standardized/readable/maintainable/side-effect-less RSpec tests. Here's what BetterSpecs says about the `let` block:
>When you have to assign a variable instead of using a  `before`  block to create an instance variable, use  `let`. Using  `let`  the variable lazy loads only when it is used the first time in the test and get cached until that specific test is finished. A really good and deep description of what  `let`  does can be found in this  [stackoverflow answer](http://stackoverflow.com/questions/5359558/when-to-use-rspec-let/5359979#5359979).

So, `let` is the preferred alternative to a `before` block. Both of these are methods we can wrap around creating our testing variables. But `let` is preferred because it runs more efficiently (and, [as Andrew pointed out](https://dev.to/andrewbrown/comment/id4h), has fewer side effects).

#### [Sidenote from BetterSpecs](http://www.betterspecs.org/#let) -- this is how they describe `let` working under-the-hood:
```ruby
# this:
let(:foo) { Foo.new }

# is very nearly equivalent to this:
def foo
  @foo ||= Foo.new
end
```
So, `let` prevents us from re-instantiating classes over and over! Definitely more efficient.

### Let's go ahead and update our code with `let`, and run the test:
```ruby
# /spec/services/random_character_generator_spec.rb

require 'rails_helper'

RSpec.describe RandomCharacterGenerator do
    
    describe "#new_character" do
		let(:player)	{ Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
	    let(:character) {
	        rcg = RandomCharacterGenerator.new
	        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
```

And run `bundle exec rspec`:
```
$ bundle exec rspec
.

Finished in 0.04259 seconds (files took 0.78975 seconds to load)
1 example, 0 failures
```
Awesome! Our test is still working (and passing) as expected.


# Using `context` for different cases

Both [Andrew](https://dev.to/andrewbrown/comment/id4h) and [BetterSpecs recommend using `context`](http://www.betterspecs.org/#contexts) to organize tests.

`context` is an alias for `describe`, so there is no under-the-hood different. **`context` exists solely to make tests more understandable to developers.**

One common way to implement `context` is to use them for different cases, such as "success" and "failure". 

## Use case: `context "success"` and `context "failure"`
Let's add some contexts to our tests to reflect when the `new_character` method succeeds or fails, based on the uniqueness of our character's name _(a validation which is NOT implemented on the Character model yet)_:
```ruby
# /spec/services/random_character_generator_spec.rb

describe "#new_character" do
		    let(:rcg)       { RandomCharacterGenerator.new }
            let(:player)    { Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
            let(:character) { rcg.new_character("Ronnie the Rat", player) }

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

        context "failure (non-unique name)" do
            # test code here
        end
    end
```

Now, let's make a `duplicate` variable by trying to create a new Character with the same name as our first `character`. In our test, we'll also expect that `duplicate` is equal to an error message string:
```ruby
# /spec/services/random_character_generator_spec.rb

    describe "#new_character" do
		    let(:rcg)       { RandomCharacterGenerator.new }
            let(:player)    { Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
            let(:character) { rcg.new_character("Ronnie the Rat", player) }
            let(:duplicate) { rcg.new_character("Ronnie the Rat", player) }

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

        context "failure (non-unique name)" do
            it "returns a message that Character is not created" do
                expect(character).to be_an_instance_of Character
                expect(duplicate).to eq "Character not created -- name already exists!"
            end
        end
    end
```
Now, our `context "failure (non-unique name)"` has an `it` block that instantiates `character` again **(remember, the `let` variables don't exist until we call them in a test!)**, and tries to create `duplicate` with the same name. Instead of being a Character, `duplicate` should equal a string containing our error message.

Let's run our tests to make sure they work, and are not passing:
```
$ bundle exec rspec
.F

Failures:

  1) RandomCharacterGenerator#new_character failure does not create a new Character instance
     Failure/Error: expect(duplicate).to eq "Character not created -- name already exists!"
     
       expected: "Character not created -- name already exists!"
            got: #<Character id: 2, name: "Ronnie the Rat", strength: 2, dexterity: 3, intelligence: 3, charisma: 1, created_at: "2019-12-08 19:05:55", updated_at: "2019-12-08 19:05:55", player_id: 1>
     
       (compared using ==)
     
       Diff:
       @@ -1,2 +1,11 @@
       -"Character not created -- name already exists!"
       +#<Character:0x00007f9e56d37880
       + id: 2,
       + name: "Ronnie the Rat",
       + strength: 2,
       + dexterity: 3,
       + intelligence: 3,
       + charisma: 1,
       + created_at: Sun, 08 Dec 2019 19:05:55 UTC +00:00,
       + updated_at: Sun, 08 Dec 2019 19:05:55 UTC +00:00,
       + player_id: 1>
       
     # ./spec/services/random_character_generator_spec.rb:63:in `block (4 levels) in <top (required)>'

Finished in 0.1283 seconds (files took 1.76 seconds to load)
2 examples, 1 failure

Failed examples:

rspec ./spec/services/random_character_generator_spec.rb:61 # RandomCharacterGenerator#new_character failure does not create a new Character instance
```
Perfect--our test works, and is failing because `duplicate` is being created as a Character instance, instead of the error-message string we were expecting.

### Let's add a uniqueness validation for `:name` to our Character model:
```ruby
# /app/models/character.rb

class Character < ApplicationRecord
    belongs_to :player
    
    validates :name, uniqueness: true
end
```

### And in `RandomCharacterGenerator.new_character()`, let's add a `rescue` that returns our error-message string:
```ruby
# /app/services/random_character_generator.rb

class RandomCharacterGenerator

    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
    rescue ActiveRecord::RecordInvalid
        return "Character not created -- name already exists!"
    end
    
end
```

### Running our tests now:
```
$ bundle exec rspec
..

Finished in 0.07993 seconds (files took 1.86 seconds to load)
2 examples, 0 failures
```
Awesome, now our `new_character` method has some better error-handling built in, and it's backed up by test coverage!

# Using `it { expect }` syntax with `context`

One final suggestion [from both Andrew](https://dev.to/andrewbrown/comment/id4h) and [the BetterSpecs section on keeping descrptions short](http://www.betterspecs.org/#short) is to simplify the `it` block's syntax with curly brackets `{}` around the `expect` statement.

When used inside a `context` block, it can make the test much more expressive and readable:
```ruby
# /spec/services/random_character_generator_spec.rb

    describe "#new_character" do
	    let(:rcg)       { RandomCharacterGenerator.new }
        let(:player)    { Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
        let(:character) { rcg.new_character("Ronnie the Rat", player) }
        let(:duplicate) { rcg.new_character("Ronnie the Rat", player) }

		# BEFORE:
        context "success" do    
            it "creates a new Character instance" do
                expect(character).to be_an_instance_of Character
            end
        end

		# AFTER:
        context "success" do    
			it { expect(character).to be_an_instance_of Character }
        end
    end
```
The second version reduces our `it` block from three lines to one, and completely eliminates the descriptive (and possibly redundant) text `"creates a new Character instance"`. Instead, we can read the test as **"the 'success' context expects variable `character` to be an instance of Character"**. Pretty self-descriptive code!

### However, this strategy is best used for one-expectation tests. Our "failure (non-unique name)" test currently relies on two `expect` lines inside the same `it` block, so this would NOT work:
```ruby
# /spec/services/random_character_generator_spec.rb

    describe "#new_character" do
	    let(:rcg)       { RandomCharacterGenerator.new }
        let(:player)    { Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
        let(:character) { rcg.new_character("Ronnie the Rat", player) }
        let(:duplicate) { rcg.new_character("Ronnie the Rat", player) }

		# this works:
        context "failure (non-unique name)" do
            it "returns a message that Character is not created" do
                expect(character).to be_an_instance_of Character
                expect(duplicate).to eq "Character not created -- name already exists!"
            end
        end

		# this does NOT work:
        context "failure (non-unique name)" do
            it { expect(character).to be_an_instance_of Character }
            it { expect(duplicate).to eq "Character not created -- name already exists!"}
        end
    end
```
The second option does not work because each `it` line creates its own scope, so `duplicate` is created as a Character successfully because the previous `character` is not instantiated in `duplicate's` scope.

### So, our final successful test code looks like this:
```ruby
# /spec/services/random_character_generator_spec.rb

require 'rails_helper'

RSpec.describe RandomCharacterGenerator do

    describe "#new_character" do
        let(:rcg)       { RandomCharacterGenerator.new }
        let(:player)    { Player.create(user_name: "Ronald McDonald", display_name: "Mac") }
        let(:character) { rcg.new_character("Ronnie the Rat", player) }
        let(:duplicate) { rcg.new_character("Ronnie the Rat", player) }

        context "success" do    
            it { expect(character).to be_an_instance_of Character}
        end

        context "failure (non-unique name)" do
            it "returns a message that Character is not created" do
                expect(character).to be_an_instance_of Character
                expect(duplicate).to eq "Character not created -- name already exists!"
            end
        end 
    end
end
```


# Conclusion
As we've covered, using `let` to create your testing variables and `context` to wrap your test cases can improve both the readability and the efficiency of your RSpec tests. We've also seen one way to simplify `it` blocks down to a single line if there's a single expectation!

### Thanks to Andrew Brown [for his incredibly helpful comment on my previous post!](https://dev.to/andrewbrown/comment/id4h)
### Additional thanks to Masaki Matsuo and Amy Pivo for helping me practice writing better RSpec tests this week!

## Further reading/links/resources about RSpec testing:
* [betterspecs.org](http://www.betterspecs.org/)
* [https://stackoverflow.com/a/5359979](https://stackoverflow.com/a/5359979)
* [https://www.ombulabs.com/blog/rails/rspec/ruby/let-vs-instance.html](https://www.ombulabs.com/blog/rails/rspec/ruby/let-vs-instance.html)
* [https://lmws.net/describe-vs-context-in-rspec](https://lmws.net/describe-vs-context-in-rspec)
* [https://thoughtbot.com/blog/my-issues-with-let](https://thoughtbot.com/blog/my-issues-with-let)

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