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:
-
Write a failing test
Run the test with the intention of failing. -
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 -
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.