Reducing Code Duplication in RSpec

Using the let Keyword in Example Groups

Erik Huang

--

RSpec is a popular testing library that has the distinction of being the most popular Ruby gem with over 200 million downloads. It is fantastic for behavior-driven development (BDD) and allows us to improve the quality of our code while acting as living documentation for the codebase. When used correctly, developers who are new to a codebase should be able to learn everything they need to know about the functionality of the program by reading the files inside thespecfolder.

I love RSpec for its beautiful english-like syntax and how it’s designed to test classes and methods in a specific domain. In this blog, we will look at some different ways to define instances of a class within a test example group, and how (spoilers) using the let keyword is best for most testing purposes.

Creating Instances in Examples

Let’s say we have a simple class in our Ruby project called Player. Player will eventually have two values: a String representing the player’s name, and an integer representing the player’s age.

player.rb

Now before we go any further with implementing our class with attributes and methods, let’s write some RSpec for the Player class. We first define an example group using the .describe method and pass in a reference to the name of the class we are testing. Next, we will create our first example within an it block and declare what behaviors we expect an instance of our class to have.

spec/player_spec.rb

Obviously, this test will fail because while we have a Player class, it has no name property than can be read using a getter, and we can’t initialize Player to have a name property of “Axel”. Let’s add a getter and constructor method to our Player class so that these tests can pass.

Great! Now our first example in RSpec:it ‘has a name’, should be passing. To recap, first we wrote a test example inside our player_spec.rb file where all the tests for Player will be. Next, we added some code to player.rb which contains the Player class in order to allow these tests to pass. This process of writing failing tests, adding code to let them pass, then streamlining the code we wrote is known as Red, Green, Refactor. Mastering this technique is the key to becoming a better developer through RSpec testing.

Now, let’s add one more example inside our spec file to test for an age attribute.

Note how Player is now initialized with two arguments: String name & Int age

Notice how we now pass in a second argument of type Int for the Player’s age in both examples. Our Player class has no age attribute nor initializes it inside the constructor, so we will go back and add more code to player.rb.

Once again, our tests are passing, but there is something wrong with our test code insideplayer_spec.rb. Notice how in both it blocks, we declare the same new instance of Player (assigning ‘Axel’ as name and 20 as age). While this doesn’t break our tests, it is clear that we are repeating ourselves in multiple blocks. If we had 50 examples for Player inside this example group, that would be a lot of repeated code!

Instead of explicitly writing code to create an object inside each example, we have some other options in RSpec.

Using Instance Variables

One way to avoid code duplication like this is employing RSpec’s before hooks. A before block, by default, performs whatever action inside of it before every single example in its example group aka the.describe block. That means that by using a Ruby instance variable, we can declare an instantiation of Player before each example runs.

Instance variables are necessary for before hooks because they can be passed into other it blocks unlike a local-scoped variable. Now, by calling @player, we can reference the name and age property like before without having to write another Player.new inside each example.

This is definitely an improvement upon our code from before, but since instance variables can be easily mutated, it is still not optimal for RSpec testing.

Using Helper Methods

Another way to instantiate a class while keeping code duplication to a minimum is through the use of helper methods. Helpers are just like any other regular method in Ruby, but we define them using the same name as the class being tested. For example, let’s write a helper method for the Player class.

Using a player helper method allows our tests to pass, but just like with instance variables, it is not the best implementation. In this case, what happens if we try to change the value of our Player’s name?

We will add this logic into our first example like so:

After initializing a player with the same values from before, we use a setter to change the value of the name property in player to be “Choco”. Before running our updated tests, let’s go ahead and also update our Player class with an attr_accessor containing both getter and setter for name.

However, when we run our tests on the updated player, we see the following error message:

Failures: 1) Player has a name 
Failure/Error: expect(player.name).to eq('Choco')
expected: "Choco"
got: "Axel"
(compared using ==)
# ./spec/player_spec.rb:11:in `block (2 levels) in <top (required)>'

What happened? Didn’t we change the name of our Player instance to “Choco”. Therein lies the issue with both instance variables and helper methods albeit in different ways: mutation.

In this case, when we try to mutate the value of name into a different value, it isn’t passed to the instance of Player inside the player method. Instead, it is trying to call a setter method on the player method itself. Since the method of player is simply contained within the example group, it does not have an attribute for name at all.

This means that any instances created within a helper method cannot be mutated for the sake of testing. So, what is the best solution for providing an instance of Player to each example block while keeping our code DRY?

Using The let Method

Enter the let keyword: a special helper method within RSpec that allows us to save and use instances of a class through memoization. To put it simply, memoization is a term in Computer Science for catching the value of an operation (usually one that is computationally expensive) and saving it for future use.

When we call let at the beginning of an example group context and pass in a symbol name and block, we can save values of an instance, string, or really any value within an RSpec test and reference it whenever we need in examples.

Here is how a let is declared:

let(:variable) { 'Hello World' }

We add let first and pass it a symbol representing the name of our variable, followed by a block containing the specific value we want let to hold (in this case, the string ‘Hello World’).

let is lazy-loaded, which means Ruby does not load the method into memory unless it is specifically called on. Let’s use let to pass an instance of Player into our example group.

Note that while declared as a symbol, player can be referenced like a normal variable

Now if we run our tests, everything passes like clockwork! Note that while name was changed in the first example block, let is extremely useful because the original value of “Axel” is still contained within other example blocks outside of its scope (ie. it ‘has an age’). In other words, this method prevents against mutation between different examples in the same example group!

Conclusion

While let is generally interchangeable with subject, another component of the RSpec library, it is a method that should be a main building block of unit testing done in Ruby. I hope you use it to maximum effect in tandem with your next Sinatra, Rails, or vanilla Ruby project!

--

--

Erik Huang

I am currently a student in the software engineering program at Flatiron school