Testing is an important part of Software Development, it can be slow to start especially as you're learning it, but it is such an important savior in the long run. As projects get bigger you can run tests to make sure new features don't break old ones and changes don't have unexpected consequences.
To get started I decided to learn and get some practice using RSpec, which is a unit test framework for the Ruby programming language. RSpec is a Behavior-driven development tool, meaning that tests written in RSpec focus on the "behavior" of an application being tested. RSpec doesn't put emphasis on, how the application works but instead on how it behaves, in other words, what the application actually does.
Here is my first try at building a simple ruby calculator using test-driven development(TDD)! TDD is a software development process relying on software requirements being converted to test cases before software is fully developed, and tracking all software development by repeatedly testing the software against all test cases. It is sometimes referred to as red-green testing because you write tests for what you want the code to do, then you run the tests to see them fail (red). Then you write the code to make the tests pass(green).
To start, make sure you have ruby and RSpec installed. You can check that is the case by running
$ ruby -v ruby 2.6.1p33 (2019-01-30 revision 66950)
$ rspec -v RSpec 3.10 - rspec-core 3.10.1 - rspec-expectations 3.10.1 - rspec-mocks 3.10.2 - rspec-support 3.10.2
If you are missing either of these you can check out the documentation for Ruby and to add RSpec you can run
$ gem install rspec
A gem is a Ruby library that you can use in your own code and you can install these using the gem command.
Next, let's make a folder in a location of your choice called calculator.
$ mkdir calculator
Let's cd into the folder and open it up with your code editor.
$ cd calculator $ code .
Next we are going to need a Gemfile that knows which version of RSpec to run.
$ touch Gemfile
In that gem file add
so it knows where to get its gems from. Then in your terminal
$ bundle add rspec
and your gemfile will be updated to have the latest version of rspec available to your project.
Now we can make folders called lib to hold the Calculator class and spec to hold the tests which is a common practice.
$ mkdir lib $ mkdir spec
Now for the fun part! Let's make a file called calculator_spec.rb, it's common practice to have your test file have the name of the file it's testing plus _spec. And another file called calculator.rb in your lib folder.
First we are going to require the file that we are testing at the top of the spec file:
Now let's write our first test! We want our Calculator to be able to add two numbers and return the result.
describe Calculator do context "Given two numbers" do it "adds the numbers using the add method" do calc = Calculator.new sum = calc.add(2,3) expect(sum).to eql(5) end end end
Woah, Nelly! What's all that! Ok. So. The word describe is an RSpec keyword. It is used to define an “Example Group”, which is a collection of tests. The describe keyword can take a class name and/or string argument, in our case we used the Calculator class name (which doesn't exist yet). You also need to pass a block argument to describe, this will contain the individual tests, or as they are known in RSpec, the “Examples”. The block is just a Ruby block designated by the Ruby do/end keywords.
So our describe Calculator block will wrap the individual tests. Inside the describe block is context, another RSpec keyword. It is very similar to the describe block, and is not mandatory, but it can help add more details to what the tests are doing. In this case, we are just testing for handling two numbers entered into the function.
The word it is another RSpec keyword used to define an “Example”, ie a test case. it is passed a string and a block argument designated with do/end (like describe and context, it can accept a class, but it would be unusual to do so). The string of it describes the expected outcome for the test. The string makes it clear what should happen when we call add on an instance of the Calculator class. As part of the RSpec philosophy, an Example is not just a test, it’s also a specification (a spec). So the Example both documents and tests the expected behavior of your Ruby code.
We then make an instance of the Calculator class so that we can call the add method on it to test if it does actually add together properly. Let's set a variable of sum equal to the result of passing in the numbers 2 and 3 into the add method.
And now, the actual test! The expect keyword is used to define an “Expectation” in RSpec. This is where we verify, that a specific expected condition has been met. The idea is that expect statements should read like normal English. You can say this aloud as “Expect the sum to equal 5”. The idea is that it's descriptive and easy to read.
The to keyword is used as part of expect statements. to is used with a dot, expect(sum).to, because it actually just a regular Ruby method. In fact, all of the RSpec keywords are really just Ruby methods.
And finally, we have the eql keyword which is a particular kind of RSpec keyword called a Matcher. Matchers are used for specifying what type of condition you are testing to be true (or false). Here we are expecting the add method to return an integer that matches 5.
All right! Let's run our test with the command rspec and the file designated in the spec folder!
$ rspec spec/calculator_spec.rb
Our error tells us that the constant Calculator is uninitalised, which makes sense because we haven't built our Calculator class yet! So in the calculator.rb file, let's make our class:
class Calculator end
Woohoo! 1 failure! This is good. We are in the red part of red/green testing. There's no method add. So let's fix that.
def add end
Now our test is telling us that the add method received 2 arguments, but was expecting 0. This is because in the test we gave it two, but in the actual code it doesn't have any arguments passed into the method So let's pass it two arguments.
def add(x,y) end
Now the test was expecting 5 to be returned, but it failed because we didn't return anything. Let's fix that.
def add(x,y) return 5 end
Woo! It passed! But...wait...that's silly, isn't it? Well, yes, you're right. If you write your tests and make them pass with the minimum amount of work, and it doesn't actually behave how you want, then you need to upgrade your tests to be a bit more robust. Let's add another test!
it "can add two different numbers" do calc = Calculator.new sum = calc.add(8,23) expect(sum).to eql(31) end
def add(x,y) return x + y end
Did anyone else notice that we initialized a new instance of the calculator class inside both it blocks? We can refactor this to using the RSpec before hook which takes in a symbol indicating the scope and a block of code to execute. The before(:each) block will run before each example.
describe Calculator do before(:each) do @calc = Calculator.new end context "Given two numbers" do it "can add the numbers using the add method" do sum = @calc.add(2,3) expect(sum).to eql(5) end it "can add two different numbers" do sum = @calc.add(8,23) expect(sum).to eql(31) end end end
Run this code again and both tests are still passing!
If you want to try adding different numbers with your new add method you can hop into an irb session in your terminal and load the file that has your calculator class, instantiate a new instance of that class storing it in a variable, then call the add method on it with the two numbers you want to add as arguments. Type exit to get out of your irb!
Go ahead and try following the red/green TDD process for subtract, multiply and divide. Don't forget! You may want to test for a user trying to divide by zero and display some kind of error message.
require './lib/calculator.rb' describe Calculator do before(:each) do @calc = Calculator.new end context "Given two numbers" do it "can add the numbers using the add method" do sum = @calc.add(2,3) expect(sum).to eql(5) end it "can add two different numbers" do sum = @calc.add(8,23) expect(sum).to eql(31) end it "can subtract the numbers using the subtract method" do expect(@calc.subtract(6,4)).to eql(2) end it "can multiply the numbers using the multiply method" do expect(@calc.multiply(3,4)).to eql(12) end it "can divide the numbers using the divide method" do expect(@calc.divide(18,3)).to eql(6) end it "gives a warning if try to divide by zero" do expect(@calc.divide(2,0)).to eql("You can't divide by zero!") end it "can provide the remainder when dividing two numbers using the modulo method" do expect(@calc.modulo(15,5)).to eql(0) end it "can square the result of a number" do expect(@calc.square(2)).to eql(4) end it "can find the squareroot of a given number" do expect(@calc.squareroot(9)).to eql(3) end it "can find the factorial of a given number" do expect(@calc.factorial(3)).to eql(6) end end end
class Calculator def add(x,y) return x + y end def subtract(x,y) return x - y end def multiply(x,y) return x * y end def divide(x,y) if y == 0 return "You can't divide by zero!" end return x / y end def modulo(x,y) return x % y end def square(x) return x * x end def squareroot(x) return Math.sqrt(x).round() end def factorial(x) result = 1 while (x > 0) result = result * x x -= 1 end return result end end
I hope this is helpful for people just dipping their toes into RSpec. Any RSpec pros out there want to comment on ways to improve the tests I wrote, feedback is much appreciated!