Rails Test Driven Development Fibonacci

In this ongoing series, we're going to explore Rails Test Driven Development by focusing on katas. In the art of Karate, a kata is a form, or choreographed sequence of movements, practiced repetitiously with the intent of making incremental improvement until one reaches mastery of that particular skill.

Test Driven Development

For this series, we'll be utilizing TDD (Test Driven Development) principles to complete each individual kata. TDD is a software development practice consisting of writing tests before the production code with the intention of producing cleaner code that makes it easier to debug. 

Test Driven Development follows 3 principles:

  1. Write a failing test
    Run the test with the intention of failing.

  2. Write the minimum code to make your test pass
    Make a change to the code that would allow the test to pass, writing as little as possible

  3. Refactor
    Remove duplication or any other code that can be changed or reorganized into reusable bits of code.

The Fibonacci Sequence

For part 1 of this series, we'll take a look at the Fibonacci sequence. The Fibonacci sequence consists of a series of numbers in which each number is the sum of the two preceding numbers. For example, the first 7 digits of the sequence is as follows: [0, 1, 1, 2, 3, 5, 8]  The recognition of the Fibonacci sequence dates as far back as 450 BC. The importance of this sequence cannot be overstated as it appears quite frequently within mathematics, but additionally within nature.

Prompt

Write a Rails script that writes out the number for a given position in the Fibonacci sequence. For example, if provided the input of '5', the program will output the 5th number in the sequence, which would be '3'. Let's begin with our first test. First, we'll need to create a file within our spec folder fibonacci_spec.rb. For the initial test, we'd like to make sure that when 1 is the input, the first number of the Fibonacci sequence, '0', is returned.

require "./app/models/fibonacci"

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end
  end
end

To solve this problem, it's important to recognize that the first 2 numbers in the Fibonacci sequence are given since it is necessary to have 2 preceding numbers to perform a Fibonacci calculation.

Run the tests, and you should see the following error:

Failures:
1) fibonacci position fibonacci finds position of valid input returns 0 when position input is 0
     Failure/Error: expect(Fibonacci.fib_finder(0)).to eq(0)
     
     NameError:
       uninitialized constant Fibonacci
     # ./spec/fibonacci_spec.rb:7:in `block (3 levels) in <top (required)>'
Finished in 0.00211 seconds (files took 0.10695 seconds to load)
1 example, 1 failure

Reading the failure log, we should notice that there is a NameError, telling us that the Fibonacci class has not yet been initialized. To solve this problem, we need to create a new class.

class Fibonacci
  def self.fib_finder(n)
     
  end
end

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns number when position input is 1' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end
  end
end

Now, if we run the test again, we should see that we now have a different error. The output from our fib_finder method is returning a nil value.

Failure/Error: expect(Fibonacci.fib_finder(0)).to eq(0)
     
       expected: 0
            got: nil
     
       (compared using ==)

Let's fix that by implementing the most simple way that we can get our test passing, which would be to have our method return the number '0'. Let's make that change and see what we get.

class Fibonacci
  def self.fib_finder(n)
     0
  end
end

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end
  end
end

After running our test, we will see that our solution will pass!

Finished in 0.00314 seconds (files took 0.1159 seconds to load)
1 example, 0 failures

Now, of course we know that this solution won't solve for any input, but the purpose of TDD is to incrementally make changes/improvements to your code as more coverage is added.  Now, let's take a look at our next input, '2'. As we stated earlier, the first 2 digits of the Fibonacci sequence are given as a requirement to perform the calculation is that there are 2 preceding digits. Since there is none, we must know that the first 2 digits of the Fibonacci sequence will be '1'. So, let's add a new test.

class Fibonacci
  def self.fib_finder(n)
     n
  end
end

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end
    it 'returns 1 when position input is 1' do
      result = Fibonacci.fib_finder(1)
      expect(result).to eq(1)
    end  
  end
end

Now onto our 3rd test, the Fibonacci of 2, which is also '1'. So, let's write our test:

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end

    it 'returns 1 when position input is 1' do
      result = Fibonacci.fib_finder(1)
      expect(result).to eq(1)
    end 
   
    it 'returns 1 when position input is 2' do
      result = Fibonacci.fib_finder(2)
      expect(result).to eq(1)
    end
  end
end

Let's run our tests and let's see what we get.

Failures:

  1) fibonnacci position fibonacci finds position of valid input returns 1 when position input is 2
     Failure/Error: expect(result).to eq(1)
     
       expected: 1
            got: 2
     
       (compared using ==)
     # ./spec/fibonacci_spec.rb:28:in `block (3 levels) in <top (required)>'

Finished in 0.02128 seconds (files took 0.205 seconds to load)
3 examples, 1 failure

For this particular test, simply returning the input value no longer works as it did for inputs '1' and '2'. Before solving, it is important to remember not to get too far ahead of ourselves by solving the problem completely, we simply want to make sure that all 3 of our tests pass. To do this, we can simply create a conditional that returns '0' if the input is '0', else we return the number '1'.

class Fibonacci
  def self.fib_finder(n)
    n == 0 ? 0 : 1
  end
end

Looks good so far, but our next test will require a bit a bit more effort as we need will need to calculate the next Fibonacci digit by adding its two preceding numbers.

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end

    it 'returns 1 when position input is 1' do
      result = Fibonacci.fib_finder(1)
      expect(result).to eq(1)
    end 
   
    it 'returns 1 when position input is 2' do
      result = Fibonacci.fib_finder(2)
      expect(result).to eq(1)
    end

    it 'returns 2 when position input is 3' do
      result = Fibonacci.fib_finder(3)
      expect(result).to eq(2)
    end
  end
end

Before implementing our solution, let's make sure that we've run our tests.


1) fibonnacci position fibonacci finds position of valid input returns 2 when position input is 3
     Failure/Error: expect(result).to eq(2)
     
       expected: 2
            got: 1
     
       (compared using ==)
     # ./spec/fibonacci_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.02222 seconds (files took 0.11964 seconds to load)
4 examples, 1 failure

As expected, this test does not pass as, per our solution, any input that isn't '0', will return a '1'.

As we learned earlier, to solve for any input greater than 1, we'll need to know the sum of 2 numbers that precede the given input. We can solve this problem by refactoring our fib_finder method into an if/else statement to handle input that we know, these include numbers 0–2. If the input is greater than these we can implement a recursive solution that looks like this.

class Fibonacci
  def self.fib_finder(n)
    if n == 0
       0
    elsif n <= 2
       1
    else
       self.fib_finder(n-1) + self.fib_finder(n-2)
    end
  end
end

After running our tests, we can see that they all pass!

Finished in 0.00623 seconds (files took 0.10912 seconds to load)
4 examples, 0 failures

For our final test, we'll choose an input that is quite larger than the previous inputs so that we can verify that our method works. The 12 number of the fibonacci sequence is 144, so let's add that to our test suite.

describe "fibonnacci position" do
  context "fibonacci finds position of valid input" do
    it 'returns 0 when position input is 0' do
      result = Fibonacci.fib_finder(0)
      expect(result).to eq(0)
    end

    it 'returns 1 when position input is 1' do
      result = Fibonacci.fib_finder(1)
      expect(result).to eq(1)
    end 
   
    it 'returns 1 when position input is 2' do
      result = Fibonacci.fib_finder(2)
      expect(result).to eq(1)
    end

    it 'returns 2 when position input is 3' do
      result = Fibonacci.fib_finder(3)
      expect(result).to eq(2)
    end

    it 'returns 144 when position input is 12' do
      result = Fibonacci.fib_finder(12)
      expect(result).to eq(144)
    end
  end
end

It appears that our last test has passed as expected!

Finished in 0.00322 seconds (files took 0.10214 seconds to load)
5 examples, 0 failures

Check out the next chapter in our Rails kata series where we'll tackle FizzBuzz using Test Driven Development.

Posts in this series

  • Rails Test Driven Development Fibonacci