Thursday, July 7, 2016

Intro to RSpec and Test-Driven Development

Need a reference text for the Ruby programming language? Get the Well-Grounded Rubyist.

RSpec has become a popular choice as a testing framework for Ruby. Let us learn how to make unit tests and use a test-driven development approach to implement a program to calculate the factorial of a number.

Factorial Review

The factorial of a number is defined as the number repeatedly multiplied by itself MINUS one until we reach the base case of 1:

4! = 4 * 3 * 2 * 1 = 24

In essence, you can define it recursively as such: the factorial of n is always that number times the factorial of (n - 1).

4! = 4 * 3! = 4 * 3 * 2! = 4 * 3 * 2 * 1! = 4 * 3 * 2 * 1 = 24

The base cases are:

1! = 1
0! = 1

Test-Driven Development

In this approach of development, we FIRST write TESTS; then, we write some code to make the tests PASS. We repeat that approach repeatedly until the development process has been complete. In summary:


1) Write tests
2) Run the tests
3) Make first test pass
4) Run the tests
5) Make second test pass
...
AND SO ON until all the tests have succeeded.

Getting Started

Create a file to hold the definition for our factorial method and call it factorial.rb:

touch factorial.rb

Now, create a folder called spec/ to hold all the unit tests:

mkdir spec

RSpec will look for tests in that directory, so it is important you follow this naming convention.

All the unit tests should be named with a suffix _spec.rb, so let us create the following test file:

touch spec/factorial_spec.rb

Writing Our First Test

We will be writing the tests inside the spec file. Let us get started and type the following into factorial_rspec.rb:

require_relative '../factorial'

describe "Factorial" do
  it "computes the factorial of 0" do
    expect(factorial(0)).to eq 1
  end
end

We need the first line to import the contents of the factorial.rb file one level up in the tree hierarchy. You need not add the .rb extension when you do that.

Next, we use a describe construct to define the block of tests for the factorial method. You typically create a describe for each method that you have. In our case, we will only be writing one method (factorial), so one describe block will suffice.

Inside the describe block, you add many unit tests using an "it" statement. These are the actual constructs that perform the tests. You will have many of these for each unit test.

When doing test-driven development, you can start off testing for the most essential elements: in the this case, we have a base case for the factorial, which is factorial(0) being equal to 1. You use the expect method to take a look at the return value of the method that was called as its argument. It will expect that to equal the integer 1.

Keep in mind that although RSpec reads a lot like English, there is no magic here: all those constructs are just Ruby methods. In our case, describe is just a method that takes on a string argument and is being passed a Ruby block (that contains the it statements). The same is true for it: it is just a method and takes a string argument and also accepts a Ruby block.

Do a web search on "RSpec Cheatsheet" or just look a this one to see all the cool things you can do with RSpec.

Running the Tests

You can run the tests from the same directory as factorial.rb using the rspec command:

rspec

RSpec will look for files in the spec/ folder and will run them. Make sure to run the rspec command one level up from the spec directory in the filesystem tree!

You will get an output similar to the following:

$ rspec
F

Failures:

  1) Factorial computes the factorial of 0
     Failure/Error: expect(factorial(0)).to eq 1

     NoMethodError:
       undefined method `factorial' for #<RSpec::ExampleGroups::Factorial:0x007ff503b68398>
     # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.00064 seconds (files took 0.13099 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/factorial_spec.rb:4 # Factorial computes the factorial of 0

So the test failed because we have not defined the factorial method. Let's go ahead and do that inside factorial.rb:

def factorial
end

If you run the tests again, you will get:

$ rspec
F

Failures:

  1) Factorial computes the factorial of 0
     Failure/Error: expect(factorial(0)).to eq 1

     ArgumentError:
       wrong number of arguments (1 for 0)
     # ./factorial.rb:16:in `factorial'
     # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.00093 seconds (files took 0.18702 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/factorial_spec.rb:4 # Factorial computes the factorial of 0

Now, there is an ArgumentError: we passed in one argument, but the method definition does not take any parameters. Let us fix that in factorial.rb:

def factorial(n)
end

Run the tests again:

$ rspec
F

Failures:

  1) Factorial computes the factorial of 0
     Failure/Error: expect(factorial(0)).to eq 1

       expected: 1
            got: nil

       (compared using ==)
     # ./spec/factorial_spec.rb:5:in `block (2 levels) in <top (required)>'

Finished in 0.05403 seconds (files took 0.1312 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/factorial_spec.rb:4 # Factorial computes the factorial of 0

Now, we got nil instead of 1 for the factorial of 0 because our method is not defined with anything in particular. Let us fix the problem by simply returning 1 from the method:

def factorial(n)
  1
end

Run the tests:

$ rspec
.

Finished in 0.0014 seconds (files took 0.18038 seconds to load)
1 example, 0 failures

Great! We finally got the test to pass. Now, we can write more tests and keep on going in the same fashion: make a test, run the tests, fix the problem, run the test again and see the test pass.

Formatting RSpec Output

You might want to format the RSpec output to display colors and more detailed information about the tests (for example, which tests actually passed). To do that, create a .rspec file in the same directory as factorial.rb:

touch .rspec

Add the following contents to the file:

--color
--format documentation

Now RSpec is going to show us a more detailed output with colors :)

Writing More Tests

Let us go back to writing tests. Write another test, now for the factorial of 1:

describe "Factorial" do
  it "computes the factorial of 0" do
    expect(factorial(0)).to eq 1
  end
  it "computes the factorial of 1" do
    expect(factorial(1)).to eq 1
  end
end

If you run the tests, you will see you are still passing them. Now, write another test:

it "computes the factorial of 2" do
  expect(factorial(2)).to eq 2
end

Run the tests again:

$ rspec

Factorial
  computes the factorial of 0
  computes the factorial of 1
  computes the factorial of 2 (FAILED - 1)

Failures:

  1) Factorial computes the factorial of 2
     Failure/Error: expect(factorial(2)).to eq 2

       expected: 2
            got: 1

       (compared using ==)
     # ./spec/factorial_spec.rb:11:in `block (2 levels) in <top (required)>'

Finished in 0.02178 seconds (files took 0.13048 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./spec/factorial_spec.rb:10 # Factorial computes the factorial of 2

Now it is your job to go to the definition of the factorial method and add some code to make the next test pass. You keep on doing that and eventually you will get to write the correct method definition. You could keep tweaking the code on and on, but as soon as you reach, say, a test that checks the factorial of 4, you can just starting writing the factorial method for real.

Here is the code for more tests:

require_relative '../factorial'

describe "Factorial" do
  it "computes the factorial of 0" do
    expect(factorial(0)).to eq 1
  end
  it "computes the factorial of 1" do
    expect(factorial(1)).to eq 1
  end
  it "computes the factorial of 2" do
    expect(factorial(2)).to eq 2
  end
  it "computes the factorial of 3" do
    expect(factorial(3)).to eq 6
  end
  it "computes the factorial of 4" do
    expect(factorial(4)).to eq 24
  end
  it "computes the factorial of 5" do
    expect(factorial(5)).to eq 120
  end
  it "computes the factorial of 6" do
    expect(factorial(6)).to eq 720
  end
  it "computes the factorial of 7" do
    expect(factorial(7)).to eq 5040
  end
end

Make sure to start with the first test that is failing and fix that one. Then, proceed to the next one, and so on. Here is the completed factorial method:

# Given an integer greater than of equal to zero
def factorial(n)
  return 1 if n <= 1

  product = 1
  index = n

  while index > 1
    product *= index
    index -= 1
  end

  product
end

In the example definition above, we return 1 for n less than or equal to 1 to take care of the base cases (0! = 1 and 1! = 1). Assume the given input is greater than or equal to zero. Then, to keep track of the continously multiplication by the number minus one, a product variable is used. The index variable is set to the given number at first and will decrease by one in every iteration. The method finally returns product, which holds the factorial of the given input, at the end.

All Tests Pass :)

$ rspec

Factorial
  computes the factorial of 0
  computes the factorial of 1
  computes the factorial of 2
  computes the factorial of 3
  computes the factorial of 4
  computes the factorial of 5
  computes the factorial of 6
  computes the factorial of 7

Finished in 0.00362 seconds (files took 0.13484 seconds to load)
8 examples, 0 failures

Conclusion

Unit testing has never been easier with RSpec for Ruby. It reads much like English and provides a nice framework to perform test-driven development. With the use of describe blocks and it blocks, you can get started writing your first tests. Make sure to write the tests first before actually implementing your code. Don't forget! :)

Looking for a reference text for Ruby? Get the Well-Grounded Rubyist.

No comments:

Post a Comment