Testing: Minitest
Overview
Unit testing is a way to check that your code works as expected.
- Manual testing is slow and error-prone in large projects
- Tests can run automatically and catch mistakes early
- Ruby has a built-in testing library called Minitest.
The idea of testing is to catch problems early and make sure your code keeps working as it grows.
Setting up a Test
-
Require the Minitest module to get access to the testing classes:
require "minitest/autorun"This loads Minitest and gives you the
Minitest::Testclass. Your test classes will inherit from this class to use its assertion methods. -
Create a test class for the code you want to check.
For example, if we have a method that adds two numbers, we can create a test class like this:
class TestMath < Minitest::TestendThis class now has all the methods we need to write tests. Each test method inside it will check a specific part of the program.
-
To write the test methods, prefix them with
test_.For example, if you want to test a
summethod, define atest_summethod inside theTestMathclass.def sum(a, b)a + bendclass TestMath < Minitest::Testdef test_sumassert_equal 7, sum(5, 2)endendThe
assert_equalchecks that the first value matches the second. If it does, the test passes. If not, it fails. -
Run the file in the terminal:
ruby test_math.rbEach dot in the output is a passing test. If a test fails, Minitest shows an
Fand tells you which assertion failed and why.Output:
Run options: --seed 40405# Running:.Finished in 0.021313s, 46.9201 runs/s, 46.9201 assertions/s.1 runs, 1 assertions, 0 failures, 0 errors, 0 skipsIn the output below, we see a single dot (
.) after "#Running". The dot represents a passing test. If there are more tests, there will also be more dots in the output. -
We can try to add another test called
test_sum_2and re-run it.require "minitest/autorun"def sum(a, b)a + bendclass TestMath < Minitest::Testdef test_sumassert_equal 7, sum(5, 2)enddef test_sum_2assert_equal 9, sum(3, 6)endendOutput:
# Running:..Finished in 0.018132s, 110.3008 runs/s, 110.3008 assertions/s.2 runs, 2 assertions, 0 failures, 0 errors, 0 skipWe can now see two dots (
..) in the output, representing the two tests that have passed. -
Add a third test called
test_sum_3. In this test, we intentionally use incorrect expected values to force a failure.require "minitest/autorun"def sum(a, b)a + bendclass TestMath < Minitest::Testdef test_sumassert_equal 7, sum(5, 2)enddef test_sum_2assert_equal 9, sum(3, 6)enddef test_sum_3assert_equal 11, sum(3, 6)endendOutput:
# Running:F..Finished in 0.018335s, 163.6213 runs/s, 163.6213 assertions/s.1) Failure:TestMath#test_sum_3 [/project/minitest.rb:17]:Expected: 11Actual: 93 runs, 3 assertions, 1 failures, 0 errors, 0 skipsWhen we run the tests, the output shows
F.., which means one test failed and two passed. Ruby also prints the failure details, clearly showing which test failed, what value was expected, and the actual value returned by the method. -
Finally, we add two more tests that are also expected to fail:
test_sum_4andtest_sum_3.require "minitest/autorun"def sum(a, b)a + bendclass TestMath < Minitest::Testdef test_sumassert_equal 7, sum(5, 2)enddef test_sum_2assert_equal 9, sum(3, 6)enddef test_sum_3assert_equal 11, sum(3, 6)enddef test_sum_4assert_equal 11, sum(1, 0)enddef test_sum_5assert_equal 11, sum(-1, nil)endendOutput:
# Running:.FFE.Finished in 0.016531s, 302.4680 runs/s, 241.9744 assertions/s.1) Failure:TestMath#test_sum_4 [/project/minitest.rb:21]:Expected: 11Actual: 12) Failure:TestMath#test_sum_3 [/project/minitest.rb:17]:Expected: 11Actual: 93) Error:TestMath#test_sum_5:TypeError: nil can't be coerced into Integer/project/minitest.rb:4:in 'Integer#+'/project/minitest.rb:4:in 'Object#sum'/project/minitest.rb:25:in 'TestMath#test_sum_5'5 runs, 4 assertions, 2 failures, 1 errors, 0 skipsHere, the output shows
.FFE., which means some tests passed, some failed, and one raised an error. The failure messages show incorrect expected values, while the error shows that passingnilcauses aTypeError.This helps us clearly see the difference between a test failure (wrong result) and a test error (invalid input that crashes the code).
Testing a Class
We can also write and run tests for a Ruby class using Minitest.
-
To start with, we'll use a
Bookwhich stores two pieces of state and exposes them using getter methods.## book.rbclass Bookattr_reader :title, :authordef initialize(title, author)@title = title@author = authorendend -
Next, we load Minitest and create a test class. Every test class must inherit from
Minitest::Test.## book.rbrequire "minitest/autorun"class Bookattr_reader :title, :authordef initialize(title, author)@title = title@author = authorendendclass TestBook < Minitest::Testend -
For the test, we'll instantiate a new
Bookobject calledbook_1and compare the expected title with the value returned by the getter. This confirms that the initializer assigned the value correctly.## book.rbrequire "minitest/autorun"class Book...endclass TestBook < Minitest::Testdef test_titlebook_1 = Book.new("The War of the Worlds", "H.G. Wells")assert_equal("The War of the Worlds", book_1.title)endend -
Tests should also be independent, so we'll add a second test called
test_author. In this test, we'll create a new objectbook_1instead of reusing the previous one.class TestBook < Minitest::Testdef test_titlebook_1 = Book.new("The War of the Worlds", "H.G. Wells")assert_equal("The War of the Worlds", book_1.title)enddef test_authorbook_1 = Book.new("The War of the Worlds", "H.G. Wells")assert_equal("H.G. Wells", book_1.author)endendEven though both tests use the same variable name,
book_1only exists inside the method where it is defined. Oncetest_titlefinishes, itsbook_1is gone. Whentest_authorruns, it creates an entirely different object from scratch.This isolation ensures that tests do not affect each other and keeps the test suite reliable.
-
Run the script:
ruby book_test.rbOutput:
# Running:..Finished in 0.016886s, 118.4441 runs/s, 118.4441 assertions/s.2 runs, 2 assertions, 0 failures, 0 errors, 0 skipsTwo dots mean both tests passed. This confirms that the class behaves as expected.
-
We can add a failing test called
test_title_2inside theTestBookclass to ensure that tests can fail as well.class TestBook < Minitest::Testdef test_title...enddef test_author...enddef test_title_2book = Book.new("The War of the Worlds", "Some Author")assert_equal "The War of the World", book.titleendendOutput:
# Running:.F.Finished in 0.018197s, 164.8635 runs/s, 164.8635 assertions/s.1) Failure:TestBook#test_title_2 [/project/book.rb:24]:Expected: "The War of the World"Actual: "The War of the Worlds"3 runs, 3 assertions, 1 failures, 0 errors, 0 skipsRunning the tests now shows one failure. This proves that the test suite is active and checking real behavior.