Test-driven calculator app using Ruby and RSpec
23 Sep 2019
I have been trying to learn Test-driven development alongside Kotlin, object-oriented programming and design patterns. Got to the point where I was burned out because all of these things are difficult and require a huge amount of mental energy.
So I decided to look for a few gentler TDD courses. By gentler, I mean in a sanguage I'm more comfortable with, and using examples that are easier for me to follow along with. A good candidate for this has been Fundamentals of TDD which I am going to work through in this entry.
Test-Driven Development (TDD) introduction
- Red, green refactor cycle.
- Red: You write tests and see them fail, red is the color of failing tests.
- Green: You make the test pass, green is the color of passing tests.
- Orange: Refactor to clean up the code and the tests, orange is just to continue the traffic light theme.
- By the end, you application will have a full suite of regression tests.
- Regression tests give you confidence to refactor, if something breaks, you know exactly where it breaks because the tests will tell you.
- While there is a bit of an investment to learning how to do TDD, and it's slower at first writing tests, the payoff is huge. It's a safety net and greatly reduces the time spent trying to figure out bugs, which is a huge maintainance cost.
Building out a calculator
Write the interface you want to interact with
The instructors started out by writing out the interface they want to interact with. This is an important theme in TDD. The interface is what you use regularly so it's important that it's easy to understand and interact with. By writing tests first, you have more control over how the interface looks. So this is a technique worth highlighting. Before you write every test, write the interface you want and comment it out to guide the way you then write your test.
require 'rspec/autorun' describe Calculator do describe "#add" do it "returns the sum of its two arguments" do calc = Calculator.new expect(calc.add(5, 10)).to eq(15) end end end
The following is a list of steps that the instructors took to write their first test in RSpec, Ruby. The code for this first test is what you see above. Some knowledge of RSpec and how the tests are structured is assumed.
- We want to describe a calculator class.
- That calculator class will have an instance method called 'add', which is indicated with a string and a pound sign followed by the name of the instance method. A convention rather than a must.
- Then we need an it block, which will describe the behaviour that we want to test.
- Then we create a calculator object and store it in a variable called 'calc'.
- Then we expect calc.add when given 5 and 10 to equal 15.
I found it interesting that they created the calculator before wrinting the test assertion which called a method on it. So they are writing the test based on how they would use it in a real application. They are programming to the interface they want to use.
Steps to passing first test.
- When running the above test for the first time, it failed with an error message that read "uninitialized constant Calculator". To solve this error, the instructors defined the Calculator class above the test code. That's all they did, nothing more.
class Calculator end
- The next error message was "undefined method 'add' for Calculator". All they did to make this test pass was create an 'add' method stub inside the calculator class.
class Calculator def add end end
- The next error says 'wrong number of arguments, given 2, expected 0. This is because the class stub doesn't specify that it needs any arguments right now. This was solved by giving the add method stub two expected arguments, as follows:
class Calculator def add(a,b) end end
- Okay, after solving this error, we have gotten to the actual expectation failure error. It took three tiny incremental steps to get to this error by writing just enough code to pass the next error message.
- The expectation error says "expected 15, got nil", because the add method we defined does not return a value yet.
- To make this test pass, we are going to do the simplest thing possible to pass the test. So we can just return a hard-coded value here, the number 15.
- We now have a passing test!
class Calculator def add(a,b) 15 end end
While the code we have written above is not very general, we have used a hard-coded value, it pushes us towards writing a new test that gorces us to write more general code.
Tests that push us from specific to general.
The next test we write pushes us to write more specific code. To write a new test, we add another 'it' block which describes the new behaviour we want to test. In our next it block, we are testing that the calculator returns the sum of two different arguments.
it "returns the sum of two different arguments" do calc = Calculator.new expect(calc.add(1,2).to eql(3) end
- When we run our test, it fails with the error message: 'expected 3, got 15'.
- In order to make this test, we need to make our code a little more generic. Again, we want to do the simplest thing we can to make this test pass.
- To make the test pass, we would again use a perverse solution, which is an if statement that says if a == 5, return 15, else return 3. This approach is known as obvious implementation. You pass the test using as simple code as possible, and use the tests to force you to be more generic.
class Calculator def add(a, b) if a == 5 15 else 3 end end end
Our next test will be similar to our last one, this time our test will say "returns the sum of another two different arguments"
- To make this test, we could do the same thing as before and write another if statement to account for the third case. In fact, we do do this to make the next error message pass.
- Now that our test is passing again it's time to take this opportunity to refactor. Refactoring is where you change the structure of your code without changing it's behaviour. In this case, we can now refactor to remove the duplication we have created in passing our test using if statements.
- We are going to return a + b instead of using all of those if statements. We could have done this after writing the previous test, but instead decided to wait until the rule of three had been observed. The rule of three is where you remove duplication after something has been duplicated not once, not twice but three times. This approach works well for more complicated programs, because often we can cause design issues by removing duplication too early by making our code more complex because of anticipated, not observed changes.