Testing can be intimidating, especially for beginners, but it really doesn't have to be that way. With a little practice, writing tests is actually fun. So let's learn RSpec from the ground up and give this testing thing a try. We'll start from scratch and learn how to write a test while solving a basic Codewars challenge using Test Driven Development (TDD). Keep in mind as we do this that the file structure I'm using is overkill for the task of solving a simple code challenge. It is, however, the way you would do it for a larger project, so I'm teaching it this way on purpose so you understand how to properly install, set up, and use RSpec.
Installation
From your project directory, run gem install rspec
.
Now run rspec --init
Setup
You will see that this command created a .rspec
file in your project directory, and a spec
folder with a spec_helper.rb
file inside it. These files are for configuration and you don't need to worry about them right now, except to say that we will need to require our spec_helper.rb
file in the file we write our tests in. More on that in a moment.
The Codewars challenge we'll be working with today is called Descending Order. Here's the brief:
Your task is to make a function that can take any non-negative integer as a argument and return it with its digits in descending order. Essentially, rearrange the digits to create the highest possible number.
Examples:
Input: 21445 Output: 54421Input: 145263 Output: 654321
Input: 1254859723 Output: 9875543221
Lets start by creating a descending_order.rb
file where we'll code our solution.
Now in your spec
directory, create a descending_order_spec.rb
file. This is where we'll write our tests.
Whenever you create a file to write tests in, it must be located in the spec
folder, and it must follow this convention: [filename]_spec.rb
. When you run RSpec, it will look in your spec
folder for any _spec.rb
files, and it'll run those. If your filenames don't follow that convention, RSpec won't be able to find them.
At the top of spec/descending_order_spec.rb
require your spec_helper
file and your descending_order.rb
file.
require_relative './spec_helper.rb'
require_relative '../descending_order.rb'
Writing the test
Test Driven Development involves writing a test that fails, and then writing code to make it pass, followed by refactoring. Because failing test output is colored red in your terminal, and passing test output is colored green, the TDD process is frequently described as Red-Green-Refactor.
First, we start with a describe
block that specifies what we're testing. In this case we'll be testing a method that we'll call descending_order
when we write it.
describe 'descending_order' do
end
Next we write an it
block with a description of what our method will do when it works. At this point we know what our method needs to do, but we don't know exactly how we're going to make that happen.
describe 'descending_order' do
it 'takes any integer and returns its digits in descending order' do
end
end
Now we call our as yet unwritten method, passing in an example argument that RSpec will use to test the code in our descending_order
method. Set that to a variable for convenience and organization. This will make it easier to test other values later.
describe 'descending_order' do
it 'takes in any integer and returns its digits in descending order' do
ordered_1 = descending_order(1587956342)
end
end
And lastly we write our expectation. Let's think about this part for a second. This is what we know at this point:
- We need to write a method called
descending_order
that takes an integer value and sorts the digits from highest to lowest. - Our test is going to call
descending_order
, passing in1587956342
as an argument. - Our method will need to then return
9876554321
. - So, since our method call is set to the variable
ordered_1
, if our code works, we expectordered_1
to equal9876554321
.
The beauty of RSpec is that it gives us several methods that we can use to set expectations for working code. In this example, we're utilizing the methods expect
, to
, and equal
.
describe 'descending_order' do
it 'takes in any integer and returns its digits in descending order' do
ordered_1 = descending_order(1587956342)
expect(ordered_1).to eq(9876554321)
end
end
And we just wrote our first test!
Running the test suite
Run the test by typing rspec
into your terminal. Alternately, and this is useful if you have a very large test suite, you can run specific test files like so: rspec spec/descending_order_spec.rb
.
Pro-tip: As you start working with larger test suites, it can become overwhelming to run your tests and see dozens of failing examples. Appending --fail-fast
or simply --f-f
will show you one failing test at a time. Like so: rspec --f-f
.
Running your tests should give you output similar to this:
Here we have our first error. Lets break it down line by line.
1) descending_order takes in any integer and returns its digits in descending order
This first line is our it
block from our test. It's telling us what the method is supposed to be doing but isn't.
Failure/Error: ordered_1 = descending_order(1587956342)
The next line tells us where in our test the failure occurred, in this case it failed when we assigned the descending_order
method to the ordered_1
variable.
NoMethodError:
undefined method 'descending_order' for ...
This line here is the actual error that occurred. Here RSpec is telling us that it can't find a descending_order
method in the file we're testing, which makes sense because we haven't written one yet.
Pro-tip: Get in the habit now of reading the entire error message, not just the specific exception. The entire error contains valuable information that will help you debug your code. I can't stress this enough. Read the whole error message.
Also, this line here, right underneath the NoMethodError
# ./spec/descending_order_spec.rb:6:in `block (2 levels) in <top (required)>'
might not look too useful at first, but this is powerful information because it tells you the exact line that failed in your test suite. In this case, the failure occurred on line 6 in the file descending_order_spec.rb
in the spec
directory, as shown here in the first half of that line:
# ./spec/descending_order_spec.rb:6
If you find yourself in a situation where you're working on an unfamiliar application with a full test suite, it's always a good idea to go into the specs and read the tests. They will give you valuable information about how the code works/is supposed to work, and you will understand the tests better when they are run.
For example, in our test we have the line expect(ordered_1).to eq(9876554321)
. This line is much easier to understand when you read the whole test and see that ordered_1
contains the argument 1587956342
. So now we understand right away that when passed 1587956342
the method is supposed to return 9876554321
. That's not obvious from the test output itself.
This is probably a good time to mention that it's extremely important that your tests be clean, organized, and understandable to anyone who will read them after you. One of the purposes of testing is to guard your code against regression. When your code needs to be debugged in the future, either by you or someone else, the test suite should be able to be read and understood right away.
So now we need to define a descending_order
method. Let's do that. Open descending_order.rb
and write the following:
def descending_order(num)
end
And run the test again.
We have a new error now, which is great news! That means we're making progress. At this point in our test, RSpec called the descending_order
method, and passed it 1587956342
. It expected the method to then return 9876554321
, but instead it returned nil
. This is because we've only defined the method, we haven't yet given it any sorting ability.
We know that Ruby has a handy #sort
method, so lets try that.
def descending_order(num)
num.sort
end
Another error! It's counter-intuitive because encountering one error after another can be frustrating, but always remember that new errors mean you're making progress toward a solution (most of the time, anyway).
The test output is telling us we tried to call #sort
on an integer, and that's not allowed. 1587956342
is a single integer value, and #sort
can't sort individual digits like that. How can we get access to the individual digits? Well, we need to split our integer, perhaps into an array. We can do that a number of ways, but the simplest way is to use the #digits
method.
def descending_order(num)
num.digits.sort
end
Running our test again gives us this output:
We're getting closer! In this case, RSpec expected 9876554321
, but got [1, 2, 3, 4, 5, 5, 6, 7, 8, 9]
, a sorted array! The problem (one of them) is that it's sorted in ascending order, and we need it sorted in descending order.
We can do that like so, using one of my favorite operators, <=>
, the spaceship:
def descending_order(num)
num.digits.sort{ |a, b| b <=> a }
end
Excellent! Our array is sorted in the right order, but now we need it to be an integer again. Let's join it.
def descending_order(num)
num.digits.sort{ |a, b| b <=> a }.join
end
We're so close now! Let's convert that string to an integer and we should be passing.
def descending_order(num)
num.digits.sort{ |a, b| b <=> a }.join.to_i
end
It works! We successfully used TDD to solve this code challenge. We're not quite done yet, though.
Refactoring the test and the code
It's important to make sure that your tests are simple, but also thorough. Let's add a couple more variables, just to make sure everything still works. Since we're solving a Codewars challenge, we'll use the ones they use.
describe 'descending_order' do
it 'takes in any integer and returns its digits in descending order' do
ordered_1 = descending_order(1587956342)
ordered_2 = descending_order(21445)
ordered_3 = descending_order(145263)
ordered_4 = descending_order(1254859723)
expect(ordered_1).to eq(9876554321)
expect(ordered_2).to eq(54421)
expect(ordered_3).to eq(654321)
expect(ordered_4).to eq(9875543221)
end
end
The tests should still be passing when you run rspec
now.
It's also important to account for edge cases. What does our method do if it's passed a single digit integer, for example? We can guess, but let's find out for sure. Because we're testing a different functionality of this method, we'll write a new test for it.
We'll start with our it
block:
it 'returns the original integer if it only has one digit' do
end
Pass in arguments and set to variables:
it 'returns the original integer if it only has one digit' do
int_0 = descending_order(0)
int_1 = descending_order(1)
end
And set our expectations:
it 'returns the original integer if it only has one digit' do
int_0 = descending_order(0)
int_1 = descending_order(1)
expect(int_0).to eq(0)
expect(int_1).to eq(1)
end
So now our full test suite for this challenge looks like this:
require_relative './spec_helper.rb'
require_relative '../descending_order.rb'
describe 'descending_order' do
it 'takes in any integer and returns its digits in descending order' do
ordered_1 = descending_order(1587956342)
ordered_2 = descending_order(21445)
ordered_3 = descending_order(145263)
ordered_4 = descending_order(1254859723)
expect(ordered_1).to eq(9876554321)
expect(ordered_2).to eq(54421)
expect(ordered_3).to eq(654321)
expect(ordered_4).to eq(9875543221)
end
it 'returns the original integer if it only has one digit' do
int_0 = descending_order(0)
int_1 = descending_order(1)
expect(int_0).to eq(0)
expect(int_1).to eq(1)
end
end
Running rspec
shows us that both of our tests are still passing. Congratulations! You just used TDD and RSpec to solve a Codewars challenge. At this point, if we had more than a three-line method, we would refactor the code, in keeping with best practices and the Red-Green-Refactor methodology. This is not necessary now, but it's an important last step that you don't want to forget. Keep these basic principles in mind as we move ahead and, with practice, Test Driven Development will become second nature.
Top comments (5)
I just happened to publish my mini-series for testing on ruby today, thought it relevant to share.
TestUnit - Writing Test Code In Ruby (1/3)
Andrew Brown 🇨🇦 ・ 4 min read
MiniTest - Writing Test Code In Ruby (2/3)
Andrew Brown 🇨🇦 ・ 5 min read
RSpec - Writing Test Code In Ruby (3/3)
Andrew Brown 🇨🇦 ・ 3 min read
Very relevant! Thank you!
What an excellent intro! thanks :)
thought you were gonna show how to create a test library like rspec from scratch.
Still, I think this post can be very informative for beginners. Thank you for the post!!
Thank you! And yes, it hadn't occurred to me that the title could be construed that way, but it totally makes sense. Thanks for the inspiration too. I would love to dig in to more advanced stuff in the future, and that would be a really great one.