Minitest is the default testing framework for Ruby. We will learn and explore writing tests by writing a method which will take in hours, minutes, seconds and returns a string formatted human readable time. In doing so we will both learn the syntax of Minitest and practice writing code in a TDD fashion.
- Discover techniques to verify our code performs as expected using automated tests
- Learn the syntax of Minitest spec-style testing
- Test code using expectations
- Practice spotting nominal and edge cases.
- Have a basic grasp on what Rake & Rakefiles are
As we work we will use the files located in this repository. Fork & Clone the repository and follow along as you go.
Our starter code has 2 files we will focus on.
clock.rb
a file we will build our method in to return a string formatted as a time.clock_test.rb
a file in which we will place tests to verify that our code meets expectations.
Run these commands in your terminal.
$ gem install minitest
$ gem install minitest-skip
$ gem install minitest-reporters
To start with we'll add some code to clock_test.rb
. We will also require minitest/autorun
# clock_test.rb
require 'minitest/autorun'
require 'minitest/reporters'
require_relative 'clock'
Minitest::Reporters.use!
Note that clock.rb
which will hold our method is currently empty.
# clock.rb
We can run the tests by typing: ruby clock_test.rb
and get the following:
$ ruby clock_test.rb
Run options: --seed 35264
# Running:
Finished in 0.000751s, 0.0000 runs/s, 0.0000 assertions/s.
0 runs, 0 assertions, 0 failures, 0 errors, 0 skips
The code we added to clock_test.rb
will automatically run any tests in the file. When we execute clock_test.rb
it scans the file for any test cases and runs them giving us the above output. Minitest is running (Yippie!), but we haven't actually written a test yet, so we get 0 runs, 0 assertions...
.
To start we'll need to create a describe
block and place our tests inside that block.
# clock_test.rb
require 'minitest/autorun'
require 'minitest/reporters'
require_relative 'clock'
Minitest::Reporters.use!
describe "clock" do
end
The describe
block indicates that this contains a suite of tests. Minitest will run these tests together and any instance variables created inside this block will be shared among the test cases.
Now we'll create an it
block which is a test-case. Each describe
block can have many it
blocks inside it and even many describe
blocks. Each it
block however should focus on testing one specific thing, while describe
blocks are used to group a set of tests.
# clock_test.rb
require 'minitest/autorun'
require 'minitest/reporters'
require_relative 'clock'
Minitest::Reporters.use!
describe "clock" do
it "will return a string" do
end
end
So we have a test-case, but it's not actually checking anything yet. So we can add an expectation which is a method call that describes a condition it expects the given item to meet. In this case we need to expect that if we call clock, it will return a string. Expectations typically start with must_
or wont_
.
So lets write our expectation
# clock_test.rb
require 'minitest/autorun'
require 'minitest/reporters'
require_relative 'clock'
Minitest::Reporters.use!
describe "clock" do
it "will return a string" do
# Arrange
hours = 8
minutes = 14
seconds = 27
# Act
time = clock(hours, minutes, seconds)
# Assert
# the `clock` method must return a string
expect(time).must_be_instance_of String
end
end
Notice the line expect(time).must_be_instance_of String
. Minitest adds an expect
method which returns an object with a bunch of expectations including the must_be_instance_of
method. Most expectations take one required argument. If the required argument to expect
is a string the expectation passes.
When we run this with ruby clock_test.rb
we get:
ruby clock_test.rb
Run options: --seed 21626
# Running:
ERROR["test_0001_will return a string", "Clock", 0.0008059999672695994]
test_0001_will return a string#Clock (0.00s)
NoMethodError: NoMethodError: undefined method `clock' for #<#<Class:0x00007faa528a19d0>:0x00007faa528e1fa8>
clock_test.rb:12:in `block (2 levels) in <main>'
1/1: [] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.00333s
1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
This is a good thing. We have our first red test. There's an error because we haven't created the Clock method yet.
Lets make the test pass by creating the clock method which returns a string.
# clock.rb
def clock(hours, minutes, seconds)
return ''
end
Now ruby clock_test.rb
gets us:
$ ruby clock_test.rb
Started with run options --seed 23098
1/1: [] 100% Time: 00:00:00, Time: 00:00:00
Finished in 0.00116s
1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
Now we have our first green/passing test.
There are a number of expectations in Minitest beyond the must_be_instance_of
method.
In the code above, we are calling must_be_instance_of
without using parentheses expect(time).must_be_instance_of String
. Ruby doesn't force you to put parentheses around a method's arguments, but the community-driven style guidelines suggest that it's good coding style to put parentheses around method arguments except for methods which are part of an internal Domain Specific Language (DSL), or basically the syntax of some kind of framework like... Minitest.
So you shouldn't put parentheses around the arguments to expectations like must_equal
, but you should around your own methods.
We are going to require that clock called with specific arguments returns a string formatted with the time properly.
# clock_test.rb
require 'minitest/autorun'
require 'minitest/reporters'
require_relative 'clock'
Minitest::Reporters.use!
describe "clock" do
it "can be called with hours, minutes and seconds as arguments" do
# Arrange
hours = 11
minutes = 14
seconds = 27
# Act
time = clock(hours, minutes, seconds)
# Assert
# the `clock` method must return a string
expect(time).must_be_instance_of String
end
it "will return a string formatted in hh:mm:ss format" do
# Arrange
hours = 11
minutes = 14
seconds = 27
# Act
time = clock(hours, minutes, seconds)
# Assert
expect((time)).must_equal "11:14:27"
end
end
Exercise Our new test is red or failing now. Update the clock
method to make the test pass. You can see a solution here.
Notice our example followed a pattern from our earlier introduction to automated tests. First we set values for hours, minutes and seconds, or arranged the situation. Then by calling the clock
method we acted, or performed the action we wanted to test. Lastly we used the expectation to assert that the result of our action was correct. This is a good example of the Arrange-Act-Assert pattern discussed earlier. Yes you can combine steps here and there's nothing wrong with that, but for clarity we are breaking the Arrange-Act-Assert pattern into discrete steps in our test.
You may remember from the Introduction to Automated Testing that it is important to test the boundaries of possible input values. In this example we should test if:
- The hour is equal to or greater than 24, should raise an error
- The minutes are greater than or equal to 60, should raise an error
- The seconds are greater than or equal to 60, should raise an error
- Any parameter is less than 0, should raise an error
Question: Are there other edge-cases we should test for? Do any other inputs cause different behaviors in the method? Talk about it with your neighbors, and click here for an answer if you are still unsure.
- You should also test numbers less than 10
- Numbers smaller than 10 should include a leading zero
We will choose to ignore for now cases where the user passes a non-integer in as a parameter. In that case it should cause a runtime error.
So we will add another it
block.
it "will display leading zeros for numbers smaller than 10" do
time = clock(11, 8, 14)
expect(time).must_equal "11:08:14"
time = clock(8, 11, 14);
expect(time).must_equal "08:11:14"
time = clock(11, 14, 8);
expect(time).must_equal "11:14:08"
end
Notice that you can use multiple expectations per test.
Exercise Now with your neighbors write code to make the test pass. Remember to run ruby clock_test.rb
to verify that your clock
method passes. If you get stuck you can refer to my sample solution.
We often want our code to raise errors if given invalid arguments. This is to prevent users of our methods from getting into trouble providing invalid data. In our case we want the user to get an error if they try to provide an invalid hour, minute or second.
it "will raise an error when given an invalid argument" do
expect {
clock(25, 14, 8)
}.must_raise ArgumentError
expect {
clock(11, 60, 8)
}.must_raise ArgumentError
expect {
clock(11, 14, 60)
}.must_raise ArgumentError
end
The must_raise
expectation takes in a block of code, and runs it checking to see if the given error was raised.
We can then update our method to make the test pass with:
# clock.rb
def clock(hours, minutes, seconds)
time_fields = [hours, minutes, seconds]
max_values = [23, 59, 59]
time_fields.each_with_index do |field, index|
if field > max_values[index]
raise ArgumentError.new("#{field} is too large")
end
end
time_fields.map! do |field|
if field < 10
"0#{field}"
else
"#{field}"
end
end
return "#{time_fields[0]}:#{time_fields[1]}:#{time_fields[2]}"
end
What is this raise
keyword?!!
raise
is used to stop execution and leave the method immediately, and it's used to indicate an error to the user. In this case it prevents the user from providing clock
invalid arguments. We will discuss raising errors later at Ada, but for now this is how you can raise ArgumentErrors to prevent users from providing invalid arguments to your methods.
Exercise: Now write a test to prevent too small inputs. Then update clock
to make the test pass. Check with your neighbor. If you get stuck you can verify your answer with the following links.
Question How could you test the code if the arguments were optional? What if clock()
should return 00:00:00
?
As our projects get larger putting all our code in the same folder gets messy, especially with test files. So we will be setting up projects like this:
We will be placing our code in the lib folder, and our tests in the test folder.
Rake is a utility Ruby developers use which lets you set up tasks, like testing and running your programs. Rake reads the Rakefile and follows the Ruby code in the Rakefile to execute listed tasks.
All we are going to use Rake for now is testing, but later Rake will do all sorts of neat stuff for us.
To install rake please enter:
gem install rake
- Move your non-testing source code into a folder called
lib
. - Move your testing test into a folder called
test
. - Change the require_relative line in the test to point to the lib folder.
- Create the following
Rakefile
in the project root directory
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs = ["lib"]
t.warning = true
t.test_files = FileList['test/*_test.rb']
end
task default: :test
You can now test your code with $ rake
$ rake
Run options: --seed 2866
# Running:
...
Finished in 0.001215s, 2469.1358 runs/s, 3292.1811 assertions/s.
3 runs, 4 assertions, 0 failures, 0 errors, 0 skips
Now you can have as many test files as you want and run all the tests with a single rake
command.
In future methods you may have methods which return floating point values, for example you could write a method which calculates an appropriate tip for a server. You should not use must_equal
to test a floating point value for equality.
Why?
Computers can only approximate floating point calculations. Rounding errors can result in values which are slightly off. Instead you should use the must_be_close_to
matcher.
For example:
describe "Example tip" do
it "calculates a 20% tip" do
# Arrange
bill = 37.50
# Act
tip = calculate_tip(bill)
# Assert - the answer will be within 0.01 of 7.5
expect(tip).must_be_close_to 7.5, 0.01
end
end
Matcher | Example | What it does | Negation |
---|---|---|---|
must_equal |
expect(obj1).must_equal obj2 |
The test succeeds if the object equals the given object. It fails otherwise. | wont_equal |
must_be |
expect(num).must_be :>, 5 |
It lets you compare the given object to another using a given operator. In this example num must be greater than 5. | wont_be |
must_be_empty |
expect(list).must_be_empty |
The test passes if the given collection is empty. | wont_be_empty |
must_be_instance_of |
expect(@bill).must_be_instance_of Bill |
The test passes if the object is an instance of the given class. | wont_be_instance_of |
must_be_kind_of |
expect(list).must_be_kind_of Enumerable |
The test fails if the object is not a kind of the argument. | wont_be_kind_of |
must_be_nil |
expect(list).must_be_nil |
The test fails if the given object is not nil. | wont_be_nil |
must_be_same_as |
expect(list).must_be_same_as another_list |
The test fails if the object is not the same as the given argument. | wont_be_same_as |
must_be_silent |
expect { obj1.do_something }.must_be_silent |
The test fails if the given block outputs something to the terminal (like using puts etc). | wont_be_silent |
must_be_close_to |
expect(Math::PI, (22.0 / 7.0)).must_be_close_to 0.01 |
In the documentation's example: expect(Math::PI, (22.0 / 7.0)).must_be_within_delta 0.01 , this expectation will pass because 22.0/7 - Math::PI == 0.001264..., which is less than the allowed delta of 0.01. |
wont_be_close_to |
must_include |
expect(list).must_include 31 |
The test fails if the collection does not contain the given value. | wont_include |
must_match |
expect(name).must_match /silly/ |
The test fails if the object doesn't match the given regular expression. | wont_match |
must_output |
expect { obj.do_something }.must_output "something" |
The test fails if the given block does not output the given value. | |
must_respond_to |
expect(bill).must_respond_to :tax |
The test fails if the object does not respond to the given method name. | wont_respond_to |
must_raise |
expect { obj1.do_something }.must_raise NoMethodError |
The test fails if the given block does not raise the given exception. | |
must_throw |
expect { obj1.do_something }.must_throw Exception |
Similar to must_raise You can probably safely ignore this, but you can see here for an explanation on the differences between raise and throw.. |
wont_throw |