© Panos Matsinopoulos 2020
P. MatsinopoulosPractical Test Automationhttps://doi.org/10.1007/978-1-4842-6141-5_3

3. Using Minitest

Panos Matsinopoulos1 
(1)
KERATEA, Greece
 
In this chapter, I am introducing you to the world of Test-Driven Development (TDD) using a popular tool called minitest (Figure 3-1). You have already studied TDD in the JavaScript world, in the two previous chapters. However, from now on, you will be living in the Ruby ecosystem. This chapter will be your foundation to better understanding the next tools that you will deal with, in the following chapters.
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig1_HTML.jpg
Figure 3-1

Minitest

Learning Goals

  1. 1.

    Learn about minitest API.

     
  2. 2.

    Learn about Test-Driven Development.

     
  3. 3.

    Learn how to write a test class in minitest.

     
  4. 4.

    Learn how to write test cases in minitest.

     
  5. 5.

    Learn how to run a single test file.

     
  6. 6.

    Learn how to run a single test.

     
  7. 7.

    Learn how to identify the errors from the failures.

     
  8. 8.

    Learn about the refactoring process.

     
  9. 9.

    Learn about the random order of test execution.

     
  10. 10.

    Learn how to have many files inside your test suite.

     
  11. 11.

    Learn how to integrate rake.

     
  12. 12.

    Learn how to load all the files of your application before starting a test.

     
  13. 13.

    Learn how to integrate minitest with RubyMine.

     

Introduction

You start your trip to test automation with one very popular family of test libraries, the minitest. The minitest is coming as a gem that you can include in your project.

Let’s start with an example.

First Unit Test

Let’s start a new project in RubyMine.

RubyMine

I use RubyMine to write my Ruby projects. It is the most advanced IDE to write your Ruby and Ruby on Rails projects. Try it out; it will boost your productivity to the sky.

Let’s call it string_combiner. Make sure it is using any Ruby 2.x. Create the necessary file .ruby-version at the root folder of your project.

Tip

I use rbenv to install and manage the different versions of Ruby my Ruby projects use.

Also, add the Gemfile with the following content (Listing 3-1).
# File: Gemfile
#
source 'https://rubygems.org'
gem 'minitest'
Listing 3-1

Gemfile

Make sure that you have a bundler gem installed and run bundle. This will install the minitest gem:
$ bundle
Fetching gem metadata from https://rubygems.org/.............
Resolving dependencies...
Installing minitest 5.14.0
Using bundler 2.1.4
Bundle complete! 1 Gemfile dependency, 2 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
$

TDD: Test-Driven Development

Having the test library installed, you will now follow the TDD approach to develop a class that will combine two strings into one. TDD means to first write the test and then implement the code that satisfies the test requirements.

Let’s create the test folder that will contain our tests:
$ mkdir test

Verify That You Can Run the Test Suite

Inside the test folder, let’s create the first test file. Let’s name it test_string_combiner.rb and have the following content (Listing 3-2).
# File: test_string_combiner.rb
#
require 'minitest/autorun'
class TestStringCombiner < Minitest::Test
  def test_foo
    assert_equal true, false
  end
end
Listing 3-2

test_string_combiner.rb Initial Content

Things that you need to pay attention to are as follows:
  1. 1.

    Your test file needs to require a minitest/autorun file.

     
  2. 2.

    There is a class that contains your tests. This class needs to derive from Minitest::Test.

     
  3. 3.

    The test class is usually named with the prefix Test, and the class under test follows. So now that you want to write a test for the class StringCombiner, you name your test class TestStringCombiner.

     
  4. 4.

    The tests themselves are public methods whose names start with the prefix test_.

     

In the preceding example, you call the minitest method assert_equal, which takes at least two arguments that need to be equal; otherwise, it will raise an error, and the test will fail.

Let’s run the test suite for this file (run it from the root folder of your project):
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 21807
# Running:
F
Finished in 0.001140s, 877.5602 runs/s, 877.5602 assertions/s.
  1) Failure:
TestStringCombiner#test_foo [test/test_string_combiner.rb:7]:
Expected: true
  Actual: false
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
The test run was executed without any problem, but your test suite has one test that failed. You can see
  1. 1.

    The test that failed

     
  2. 2.

    The line on which the test failed: [test/test_string_combiner.rb:7]

     
  3. 3.

    What value was expected

     
  4. 4.

    What value was returned

     

And now let’s correct the assertion because it was incorrectly written on purpose.

The file test/test_string_combiner.rb needs to be like Listing 3-3.
# File: test_string_combiner.rb
#
require 'minitest/autorun'
class TestStringCombiner < Minitest::Test
  def test_foo
    assert_equal true, true
  end
end
Listing 3-3

Update test_string_combiner.rb

And if you run it again, you will see that all tests succeed:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 53692
# Running:
.
Finished in 0.000928s, 1077.5549 runs/s, 1077.5549 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
$

Perfect!

Test Real Requirements

Now that you know that the test suite runs without problem, let’s try to write some real requirements in the form of tests for the StringCombiner class. The following is the new version of the file test_string_combiner.rb (Listing 3-4).
# File: test_string_combiner.rb
#
require 'minitest/autorun'
class TestStringCombiner < Minitest::Test
  def test_combines_two_strings
    string1 = 'foo'
    string2 = 'bar'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'fboaor', string_combiner.combine
  end
end
Listing 3-4

Real Requirements

You can see that
  1. 1.

    It has a method, test_combines_two_strings, that demonstrates how the StringCombiner object should work.

     
  2. 2.

    It first prepares the test data (lines 7–9).

     
  3. 3.

    It then fires the test method under test.

     

Red Light

If you run this, it will fail:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 51206
# Running:
E
Finished in 0.001031s, 970.0986 runs/s, 0.0000 assertions/s.
  1) Error:
TestStringCombiner#test_combines_two_strings:
NameError: uninitialized constant TestStringCombiner::StringCombiner
    test/test_string_combiner.rb:9:in `test_combines_two_strings'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
$

The error NameError : uninitialized constant TestStringCombiner::StringCombiner is very clear. On line 9, you have StringCombiner. It is a constant, and Ruby tries to resolve it within the TestStringCombiner class. But it does not exist. This should be your production code that is under test.

Let’s create it. Inside the root folder of your project, create the file string_combiner.rb and define that class (Listing 3-5).
# File: string_combiner.rb
#
class StringCombiner
end
Listing 3-5

string_combiner.rb First Version

Then try to run the test again. You will get the same error. So, although you have created the class StringCombiner, the test cannot find it. But this is normal. The string_combiner.rb file needs to be required so that the constant is found.

Update the test_string_combiner.rb file to require the file. Add the line require_relative '../string_combiner' below the existing require command, and try to run the test again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 40013
# Running:
E
Finished in 0.000982s, 1018.4668 runs/s, 0.0000 assertions/s.
  1) Error:
TestStringCombiner#test_combines_two_strings:
ArgumentError: wrong number of arguments (2 for 0)
    test/test_string_combiner.rb:10:in `initialize'
    test/test_string_combiner.rb:10:in `new'
    test/test_string_combiner.rb:10:in `test_combines_two_strings'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
$

Things are getting better. You just got rid of the error that had to do with the name StringCombiner. Now, the error is again on line 10 where you initialize the StringCombiner by using two arguments instead of zero.

Since you are in TDD mode and the test is telling you that the StringCombiner class needs to be initialized using two arguments, you are going to do that. The following is the new version of the string_combiner.rb file (Listing 3-6).
# File: string_combiner.rb
#
class StringCombiner
  def initialize(string1, string2)
  end
end
Listing 3-6

New Version of string_combiner.rb

You have added the correct initializer. Now, let’s try to run the test again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 34366
# Running:
E
Finished in 0.000933s, 1071.7689 runs/s, 0.0000 assertions/s.
  1) Error:
TestStringCombiner#test_combines_two_strings:
NoMethodError: undefined method `combine' for #<StringCombiner:0x007fca49acac40>
    test/test_string_combiner.rb:13:in `test_combines_two_strings'
1 runs, 0 assertions, 0 failures, 1 errors, 0 skips
$

A new error this time is telling you that the method combine is not something that the object of class StringCombiner responds to.

Cool! That was expected! Remember…you are doing TDD.

Let’s add the method. The following is the new version of the file string_combiner.rb (Listing 3-7).
# File: string_combiner.rb
#
class StringCombiner
  def initialize(string1, string2)
  end
  def combine
  end
end
Listing 3-7

Updated string_combiner.rb

Let’s now run the test again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 9827
# Running:
F
Finished in 0.001069s, 935.4047 runs/s, 935.4047 assertions/s.
  1) Failure:
TestStringCombiner#test_combines_two_strings [test/test_string_combiner.rb:13]:
Expected: "fboaor"
  Actual: nil
1 runs, 1 assertions, 1 failures, 0 errors, 0 skips
$

Now, you don’t have errors. You have a failing test. It is clearly mentioning that the assertion failed on line 13. The result of the call to string_combiner.combine was nil, when it should have been fboaor.

So you need to change the implementation of the combine method to carry out the correct work. Let’s do that. The following is the new version of the string_combiner.rb file (Listing 3-8).
# File: string_combiner.rb
#
class StringCombiner
  def initialize(string1, string2)
  end
  def combine
    'fboaor'
  end
end
Listing 3-8

First combine Method Implementation

If you run this, you will see that your test is now green, that is, it is running successfully:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 23160
# Running:
.
Finished in 0.000996s, 1004.3761 runs/s, 1004.3761 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
$

Bingo! Your project is ready. Let’s ship it to the QA (Quality Assurance) team to approve it.

Without any doubt, this implementation will not pass any QA check. The test that you have written is quite poor and does not make anyone confident that the implementation would work for any combination of strings.

Make the Test Better

You can put one more test in the test suite that will prove that your code is not well implemented.

Let’s do that. See the following version of test/test_string_combiner.rb (Listing 3-9).
# File: test_string_combiner.rb
#
require 'minitest/autorun'
require_relative '../string_combiner'
class TestStringCombiner < Minitest::Test
  def test_combines_two_strings
    string1 = 'foo'
    string2 = 'bar'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'fboaor', string_combiner.combine
  end
  def test_combines_two_string_case_2
    string1 = 'john'
    string2 = 'woo'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'jwoohon', string_combiner.combine
  end
end
Listing 3-9

Add One More Test

If you run your tests again, you will see that the new test will fail:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 29200
# Running:
F.
Finished in 0.000979s, 2042.4837 runs/s, 2042.4837 assertions/s.
  1) Failure:
TestStringCombiner#test_combines_two_string_case_2 [test/test_string_combiner.rb:22]:
Expected: "jwoohon"
  Actual: "fboaor"
2 runs, 2 assertions, 1 failures, 0 errors, 0 skips
$
It is obvious that, first, you have to improve the test strategy for this particular problem. Let’s see a version of test/test_string_combiner.rb that has more test cases (Listing 3-10).
# File: test_string_combiner.rb
#
require 'minitest/autorun'
require_relative '../string_combiner'
class TestStringCombiner < Minitest::Test
  def test_combines_two_strings_with_equal_length
    string1 = 'foo'
    string2 = 'bar'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'fboaor', string_combiner.combine
  end
  def test_combines_two_strings_first_string_longer_than_second
    string1 = 'jonathan'
    string2 = 'woo'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'jwoonoathan', string_combiner.combine
  end
  def test_combines_two_strings_first_string_shorter_than_second
    string1 = 'maria'
    string2 = 'jonathan'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'mjaorniaathan', string_combiner.combine
  end
  def test_combines_two_strings_first_is_blank
    string1 = ''
    string2 = 'maria'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'maria', string_combiner.combine
  end
  def test_combines_two_strings_second_is_blank
    string1 = 'john'
    string2 = ''
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'john', string_combiner.combine
  end
  def test_combines_two_strings_first_is_nil
    string1 = nil
    string2 = 'maria'
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'maria', string_combiner.combine
  end
  def test_combines_two_strings_second_is_nill
    string1 = 'john'
    string2 = nil
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal 'john', string_combiner.combine
  end
  def test_combines_two_strings_both_are_nil
    string1 = nil
    string2 = nil
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    assert_equal nil, string_combiner.combine
  end
  def test_combines_two_strings_general_case
    string1 = ('a'..'z').to_a.sample(rand(100)).join
    string2 = ('a'..'z').to_a.sample(rand(100)).join
    string_combiner = StringCombiner.new(string1, string2)
    # fire
    expected_result = ''
    # first take the chars from the first string and interpolate the chars of the second.
    string1.split('').each_with_index do |char, index|
      expected_result = "#{expected_result}#{char}#{string2[index]}"
    end
    # if the second string is longer than the first, then we have some second string chars that we have to amend.
    # Note that given a string "x", the "x[-5..-1]", for example, takes the last 5 chars of the string.
    if string2.length > string1.length
      expected_result = "#{expected_result}#{string2[-(string2.length - string1.length)..-1]}"
    end
    assert_equal expected_result, string_combiner.combine
  end
end
Listing 3-10

More Test Cases

The preceding test set includes examples of all the case types that you need to cover: the first string longer than the second, the other way around, nil strings, blank strings, and so on. It also has a very general case, with random strings with length up to 99 characters.

It really seems a much better test coverage than the original one.

Let’s run the tests again:
Run options: --seed 4718
# Running:
FFFUse assert_nil if expecting nil from test/test_string_combiner.rb:77:in `test_combines_two_strings_both_are_nil'. This will fail in MT6.
.FFFFF
Finished in 0.010968s, 820.5367 runs/s, 820.5367 assertions/s.
  1) Failure:
TestStringCombiner#test_combines_two_strings_with_equal_length [test/test_string_combiner.rb:13]:
Expected: "fboaor"
  Actual: nil
  2) Failure:
TestStringCombiner#test_combines_two_strings_second_is_blank [test/test_string_combiner.rb:50]:
Expected: "john"
  Actual: nil
  3) Failure:
TestStringCombiner#test_combines_two_strings_general_case [test/test_string_combiner.rb:97]:
--- expected
+++ actual
@@ -1 +1 @@
-"pnkmfieerdvsuogljjtwoayzxtlqakbumgvbxfychrp"
+nil
  4) Failure:
TestStringCombiner#test_combines_two_strings_first_string_longer_than_second [test/test_string_combiner.rb:22]:
Expected: "jwoonoathan"
  Actual: nil
  5) Failure:
TestStringCombiner#test_combines_two_strings_second_is_nill [test/test_string_combiner.rb:68]:
Expected: "john"
  Actual: nil
  6) Failure:
TestStringCombiner#test_combines_two_strings_first_string_shorter_than_second [test/test_string_combiner.rb:31]:
Expected: "mjaorniaathan"
  Actual: nil
  7) Failure:
TestStringCombiner#test_combines_two_strings_first_is_nil [test/test_string_combiner.rb:59]:
Expected: "maria"
  Actual: nil
  8) Failure:
TestStringCombiner#test_combines_two_strings_first_is_blank [test/test_string_combiner.rb:40]:
Expected: "maria"
  Actual: nil
9 runs, 9 assertions, 8 failures, 0 errors, 0 skips

As you can see, only one test succeeded – the first one. This is expected. You didn’t change anything in the implementation of the StringCombiner in any way.

Make Them Green

Having written all the test cases that you want to cover, you can then proceed into the implementation of StringCombiner that will make your test cases go green. Here is the version of the string_combiner.rb file that does that (Listing 3-11).
# File: string_combiner.rb
#
class StringCombiner
  def initialize(string1, string2)
    @string1, @string2 = string1, string2
  end
  def combine
    result = ''
    # first take the chars from the first string and interpolate the chars of the second.
    string1.split('').each_with_index do |char, index|
      result = "#{result}#{char}#{string2[index]}"
    end
    if string2.length > string1.length
      result = "#{result}#{string2[-(string2.length - string1.length)..-1]}"
    end
    result
  end
  private
  attr_reader :string1, :string2
end
Listing 3-11

Version That Satisfies Tests

The new version of StringCombiner
  1. (1)

    Stores the initialization strings into instance variables

     
  2. (2)

    Has an implementation of the #combine method that combines the two strings, hopefully giving correct results. Tests will prove that.

     
Let’s run the tests again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 21146
# Running:
E.E...E..
Finished in 0.001433s, 6279.8292 runs/s, 4186.5528 assertions/s.
  1) Error:
TestStringCombiner#test_combines_two_strings_first_is_nil:
NoMethodError: undefined method `split' for nil:NilClass
    /Users/...string_combiner.rb:11:in `combine'
    test/test_string_combiner.rb:59:in `test_combines_two_strings_first_is_nil'
  2) Error:
TestStringCombiner#test_combines_two_strings_second_is_nill:
NoMethodError: undefined method `[]' for nil:NilClass
    /Users/...string_combiner.rb:12:in `block in combine'
    /Users/...string_combiner.rb:11:in `each'
    /Users/...string_combiner.rb:11:in `each_with_index'
    /Users/...string_combiner.rb:11:in `combine'
    test/test_string_combiner.rb:68:in `test_combines_two_strings_second_is_nill'
  3) Error:
TestStringCombiner#test_combines_two_strings_both_are_nil:
NoMethodError: undefined method `split' for nil:NilClass
    /Users/...string_combiner.rb:11:in `combine'
    test/test_string_combiner.rb:77:in `test_combines_two_strings_both_are_nil'
9 runs, 6 assertions, 0 failures, 3 errors, 0 skips
$

As you can read on the last line of the output, you had six successful tests and three errors. Note that these are not failures. These are errors. Error is when the test fails with a runtime error, but not with an assertion error. Failure is when the test runs successfully, but its assertion fails.

Let’s see the first error:
1) Error:
TestStringCombiner#test_combines_two_strings_first_is_nil:
NoMethodError: undefined method `split' for nil:NilClass
    /Users/...string_combiner.rb:11:in `combine'
    test/test_string_combiner.rb:59:in `test_combines_two_strings_first_is_nil'
You are trying to call a split method on something that is nil. This happens on line 11 of the string_combiner.rb file:
string1.split('')

Also, the test that is failing is the test_combines_two_strings_first_is_nil.

I guess that it is easy for you to figure out why the test is failing. It is because the first string is nil and calling the split on it raises the NoMethodError . Hence, our string_combiner.rb needs to cater for nil value for string inputs.

Let’s improve the StringCombiner#combine implementation with a line return string2 if string1.nil? at the top of its body:
  def combine
    return string2 if string1.nil?
    ...

This tells the combine method to return the second string, if the first string is nil.

Let’s run the tests again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 28772
# Running:
.EUse assert_nil if expecting nil from test/test_string_combiner.rb:77:in `test_combines_two_strings_both_are_nil'. This will fail in MT6.
.......
Finished in 0.001320s, 6820.0108 runs/s, 6062.2318 assertions/s.
  1) Error:
TestStringCombiner#test_combines_two_strings_second_is_nill:
NoMethodError: undefined method `[]' for nil:NilClass
    /Users/...string_combiner.rb:14:in `block in combine'
    /Users/...string_combiner.rb:13:in `each'
    /Users/...string_combiner.rb:13:in `each_with_index'
    /Users/...string_combiner.rb:13:in `combine'
    test/test_string_combiner.rb:68:in `test_combines_two_strings_second_is_nill'
9 runs, 8 assertions, 0 failures, 1 errors, 0 skips
$
Better. You now have only one error and eight tests that are green. Let’s see how you can fix the error left. The error is again NoMethodError . You are trying to call [], that is, the array or hash access operator on something that is nil. The line that is failing is line 14:
result = "#{result}#{char}#{string2[index]}"

Reading the error message and the line at error carefully, it seems that string2 is nil when this line of code is being executed. The test that is failing is TestStringCombiner#test_combines_two_strings_second_is_nill: which confirms that string2 is indeed nil.

Let’s enhance our #combine method to cater for that too:
  def combine
    return string2 if string1.nil?
    return string1 if string2.nil?
    ...

Now, we are returning string1 (which is not nil) if string2 is nil.

Let’s run the tests again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 36495
# Running:
DEPRECATED: Use assert_nil if expecting nil from test/test_string_combiner.rb:77. This will fail in Minitest 6.
.........
Finished in 0.001316s, 6841.4168 runs/s, 6841.4168 assertions/s.
9 runs, 9 assertions, 0 failures, 0 errors, 0 skips
$

Bingo! All of your nine tests have run successfully. With the existing test coverage, you are pretty confident that your implementation satisfies the functional requirements exactly as they are specified inside the test suite.

assert_nil

You may have noticed the warning that you are getting when you run your tests:
DEPRECATED: Use assert_nil if expecting nil from test/test_string_combiner.rb:77. This will fail in Minitest 6.
It is telling you that you should be using the method assert_nil whenever you want to compare a value against nil. Also, it is telling you which test you have to correct. This is the test test_combines_two_strings_both_are_nil. Line 77 of test/test_string_combiner.rb is
assert_equal nil, string_combiner.combine
Let’s do that. The new version should have
assert_nil string_combiner.combine

on line 77.

And let’s run the tests again:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 60984
# Running:
.........
Finished in 0.001388s, 6484.2853 runs/s, 6484.2853 assertions/s.
9 runs, 9 assertions, 0 failures, 0 errors, 0 skips
$

Perfect! No warning anymore. Remember that assert_nil is there for you to assert that something is nil.

Refactor

Now that you have your test suite green, you can proceed with refactoring, the process that you use to make your implementation code cleaner or generally change your implementation code without changing its functionality. The functionality offered will be the same and specified precisely by your test suite. You will make changes to the implementation of the StringCombiner#combine method; but, for any change that you make, you will make sure that it continues to offer the same functionality and all the tests are running green.

The new, refactored implementation of the method #combine is shown in Listing 3-12.
def combine
  return string2 if string1.nil?
  return string1 if string2.nil?
  # First interpolate the second string into the first.
  result = string1.
      split('').
        each_with_index.
        reduce('') {|result, (item, index)| result = "#{result}#{item}#{string2[index]}"}
  # If we have characters left, then append them to the result
  if string2.length > string1.length
    result = "#{result}#{string2[-(string2.length - string1.length)..-1]}"
  end
  result
end
Listing 3-12

New Refactored Implementation

This implementation uses the #reduce() method instead of iterating with #each. Otherwise, it is not different from the previous.

But as I said, the fact that you have good test coverage makes you very confident to proceed with changes in your code. You can now run the tests again and check whether you have broken anything:
$ bundle exec ruby test/test_string_combiner.rb
Run options: --seed 46505
# Running:
Finished in 0.001317s, 6831.1403 runs/s, 6831.1403 assertions/s.
9 runs, 9 assertions, 0 failures, 0 errors, 0 skips
$

As you can see, everything is still green, passing.

Running a Single Test

Sometimes you may want to run a single test from your test suite. How can you do that?

You use the option --name followed by the method name that corresponds to the test that you want to run. For example, let’s suppose that you want to run the test on line 25, which is the method with the name test_combines_two_strings_first_string_shorter_than_second. This is how you can run this particular test only:
$ bundle exec ruby test/test_string_combiner.rb --name test_combines_two_strings_first_string_shorter_than_second
Run options: --name test_combines_two_strings_first_string_shorter_than_second --seed 18437
# Running:
.
Finished in 0.000915s, 1093.0144 runs/s, 1093.0144 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips
$

As you can see from the output, the runner executed only one test, the one that you have specified on the command line with the option –name.

seed

You may have noticed the lines Run options: --seed XXXXX that are printed at the start of each test suite run. This is a feature that minitest offers that picks up your tests and runs them in random order. So, every time you run your tests, they are not executed in the same order. This is actually very good. Your tests should be independent to each other. Each test should be isolated and should succeed no matter what was the test that ran before it.

If your tests have execution interdependencies and a test might fail if a specific test runs before it, either exactly before or not, but succeeds when run alone, then, this random order of execution of the whole test suite will finally reveal this interdependence problem, maybe not on the first or second run, but at a run in the multiple runs of your test suite.

When your test suite has a failing test that is not failing if this test runs alone, then you can use the seed number that the test suite prints at the start of the execution and run the tests in the same (failing) order again and again until you find out the reason why your test is failing.

This is how you can start the test/test_string_combiner.rb using the seed 46505, for example, a seed from a previous run:
$ bundle exec ruby test/test_string_combiner.rb --seed 46505
Run options: --seed 46505
# Running:
.........
Finished in 0.001095s, 8218.8553 runs/s, 8218.8553 assertions/s.
9 runs, 9 assertions, 0 failures, 0 errors, 0 skips
$

You can see that giving the --seed 46505, you have asked minitest to run the test suite with the execution order that corresponds to that number.

Multiple Files

Usually, you don’t put all of your tests inside a single file. You break the unit tests into one file per class tested. Let’s see an example of that. Let’s suppose that you want another class in your project called Customer. You will put its functional requirements inside the file test/test_customer.rb. The file and its functional requirements are shown in Listing 3-13.

Remember, you are doing TDD, Test-Driven Development. So you first write the requirements in the form of tests, and then you write the implementation that satisfies these requirements.
# File: test/test_customer.rb
#
require 'minitest/autorun'
class TestCustomer < Minitest::Test
  def test_has_public_first_name
    customer = Customer.new('John', 'Papas')
    assert_equal 'John', customer.first_name
  end
  def test_has_public_last_name
    customer = Customer.new('John', 'Papas')
    assert_equal 'Papas', customer.last_name
  end
  def test_has_public_name_combining_first_and_last_name
    customer = Customer.new('John', 'Papas')
    assert_equal 'John Papas', customer.name
  end
end
Listing 3-13

Tests for the Customer class

You now understand how easy it is to write the basic functional requirements, the unit tests for your class Customer. You have written three tests that have to do with the first name, last name, and full name of the customer. That’s a good start, and let’s run the tests for the particular file:
$ bundle exec ruby test/test_customer.rb
Run options: --seed 17242
# Running:
EEE
Finished in 0.000952s, 3149.9006 runs/s, 0.0000 assertions/s.
  1) Error:
TestCustomer#test_has_public_name_combining_first_and_last_name:
NameError: uninitialized constant TestCustomer::Customer
    test/test_customer.rb:19:in `test_has_public_name_combining_first_and_last_name'
  2) Error:
TestCustomer#test_has_public_last_name:
NameError: uninitialized constant TestCustomer::Customer
    test/test_customer.rb:13:in `test_has_public_last_name'
  3) Error:
TestCustomer#test_has_public_first_name:
NameError: uninitialized constant TestCustomer::Customer
    test/test_customer.rb:7:in `test_has_public_first_name'
3 runs, 0 assertions, 0 failures, 3 errors, 0 skips
$
You can see that you have three errors. This is because the Customer class has not been defined. Let’s define it and require it from the test file. Also, you will implement the class so that all tests are going green. Here is the customer.rb file (Listing 3-14).
# File: customer.rb
#
class Customer
  def initialize(first_name, last_name)
    @first_name, @last_name = first_name, last_name
  end
  def name
    "#{first_name} #{last_name}"
  end
  attr_reader :first_name, :last_name
end
Listing 3-14

customer.rb Content

Then make sure that your test/test_customer.rb file has the require_relative '../customer' line at the top:
# File: test/test_customer.rb
#
require 'minitest/autorun'
require_relative '../customer'
...
Run the tests again:
$ bundle exec ruby test/test_customer.rb
Run options: --seed 63265
# Running:
...
Finished in 0.001003s, 2991.5966 runs/s, 2991.5966 assertions/s.
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
$

Perfect! The tests that have to do with the Customer class are running green. What about the StringCombiner tests? Are they still green? Generally, after having finished with a set of unit tests, you usually want to run the whole test suite. How can you do that when you have multiple test files?

I guess that the first thing that might come to your mind is to create a single test/test_runner.rb file that would load all the test files that your suite has (Listing 3-15).
# File: test/test_runner.rb
#
load 'test/test_customer.rb'
load 'test/test_string_combiner.rb'
Listing 3-15

test_runner.rb Loads All Test Files

As you can see in the preceding code, the test/test_runner.rb file will load all the test files. So you only have to invoke that file with your ruby call:
$ bundle exec ruby test/test_runner.rb
Run options: --seed 64322
# Running:
............
Finished in 0.001574s, 7625.4675 runs/s, 7625.4675 assertions/s.
12 runs, 12 assertions, 0 failures, 0 errors, 0 skips
$

Perfect. 12 runs, 12 assertions – 9 from the test/test_string_combiner.rb file and 3 from the test/test_customer.rb file.

However, this has the minor caveat that you have to update this file every time you add a new test file. Can you avoid that?

Yes. Look at the following version of the test/test_runner.rb file (Listing 3-16).
# File: test/test_runner.rb
#
Dir.glob("test/**/test_*.rb").each do |file|
  load file unless file == 'test/test_runner.rb'
end
Listing 3-16

New Version of the test/test_runner.rb File

This one loads all the files inside the folder test and its subfolders. The files need to have filenames that start with the prefix test_. Also, it excludes, from loading, the test/test_runner.rb itself.

If you run this, you will see all the tests being run successfully again:
$ bundle exec ruby test/test_runner.rb
Run options: --seed 40052
# Running:
............
Finished in 0.001574s, 7625.4675 runs/s, 7625.4675 assertions/s.
12 runs, 12 assertions, 0 failures, 0 errors, 0 skips
$

Using rake

Alternatively, instead of having the test_runner.rb file, you can use rake and write a custom task that would do the job for you.

First, add the gem 'rake' inside your Gemfile (Listing 3-17).
# File: Gemfile
#
source 'https://rubygems.org'
gem 'minitest'
gem 'rake'
Listing 3-17

Gemfile with rake

and execute bundle to install rake:
$ bundle
Fetching gem metadata from https://rubygems.org/
Resolving dependencies...
Using rake 13.0.1
Using bundler 2.1.4
Using minitest 5.14.0
Bundle complete! 2 Gemfile dependencies, 3 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
$
Then, remove the file test/test_runner.rb, because you will not need it. And create the Rakefile with the following content (Listing 3-18).
# File: Rakefile
#
desc "Run all the test in your test folder"
task :test do
  Dir.glob("test/**/test_*.rb").each do |file|
    load file
  end
end
task default: :test
Listing 3-18

Rakefile

Now, run bundle exec rake on your project folder:
$ bundle exec rake
Run options: --seed 14731
# Running:
............
Finished in 0.002176s, 5514.1560 runs/s, 5514.1560 assertions/s.
12 runs, 12 assertions, 0 failures, 0 errors, 0 skips
$

Perfect! bundle exec rake will load the whole test suite and run it.

Require All Files of Your Project

You can see that on each one of the test files, we are requiring the project file that the test is about to test. For example, the test file test/test_string_combiner.rb is requiring the file string_combiner.rb with the command require_relative 'string_combiner'. Or the file test/test_customer.rb will require the file customer.rb with the command require_relative 'customer'.

However, I believe that this is not a good practice, because when a test starts, the Ruby memory has not loaded all the files that comprise your application. So the context of loaded constants and classes is not the same as it will, probably, be at your production environment. In your production environment, usually, you want all the classes loaded at boot-up of your application, before it starts doing the actual work.

Hence, in order to have your test suite match your production environment as much as possible, you might want to load all your project files before any test runs.

Let’s try one technique that you can do that:
  1. (1)

    Let’s require all the project files using an all.rb file like the following (Listing 3-19).

     
# File: all.rb
#
require_relative 'customer'
require_relative 'string_combiner'
Listing 3-19

all.rb

  1. (2)

    Then you create the file test/test_helper.rb that will require the all.rb and whatever other file your test files need (Listing 3-20).

     
# File: test/test_helper.rb
#
require 'minitest/autorun'
require_relative '../all'
Listing 3-20

test/test_helper.rb File

  1. (3)

    Update the Rakefile so that, first, it requires the test/test_helper.rb file and then it excludes the test/test_helper.rb file from being loaded as a test file (Listing 3-21).

     
# File: Rakefile
#
desc "Run all the test in your test folder"
task :test do
  require_relative 'test/test_helper'
  Dir.glob("test/**/test_*.rb").each do |file|
    load file unless file == 'test/test_helper.rb'
  end
end
task default: :test
Listing 3-21

Rakefile requiring test_helper

  1. (4)

    Then make sure you remove the require and require_relative statements from your test files.

     
  2. (5)

    Run the tests. You are ready!

     
$ bundle exec rake
Run options: --seed 39148
# Running:
............
Finished in 0.002229s, 5383.7975 runs/s, 5383.7975 assertions/s.
12 runs, 12 assertions, 0 failures, 0 errors, 0 skips
$

Now that you have done those changes, there is a problem in case you want to run a single test.

Try this command:
$ bundle exec ruby test/test_string_combiner.rb --name test_combines_two_strings_first_string_shorter_than_second
You will get this error here:
Traceback (most recent call last):
test/test_string_combiner.rb:3:in `<main>': uninitialized constant Minitest (NameError)
This is now due to the fact that you have removed the file requirements from the test files. Hence, the test/test_helper.rb is not required. How can we remedy this? You need to call ruby command with the option -I and the option -r. Here is how:
$ bundle exec ruby -I. -r test/test_helper test/test_string_combiner.rb --name test_combines_two_strings_first_string_shorter_than_second
If you do that, then the tests will run successfully:
Run options: --name test_combines_two_strings_first_string_shorter_than_second --seed 27061
# Running:
.
Finished in 0.000853s, 1172.3330 runs/s, 1172.3330 assertions/s.
1 runs, 1 assertions, 0 failures, 0 errors, 0 skips

The -I. tells ruby that there is one more path, the current path (. corresponds to the current path), to be added to the load path (i.e., to the $LOAD_PATH variable); and the -r <file> tells ruby which file to require before running the script that is given as the next argument.

RubyMine Integration

It’s good to know how to run tests from the command line, but since you are using RubyMine, I will show you how RubyMine can easily integrate with minitest.

Let’s create a RubyMine project for the string combiner project, like the following (Figure 3-2).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig2_HTML.jpg
Figure 3-2

RubyMine Project for String Combiner

Then, right-click the project name and select Run All tests in string_combiner (Figure 3-3).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig3_HTML.jpg
Figure 3-3

Right-Click and Select Run ➤ All tests in string_combiner

When you do that, you will probably have errors that would tell you that RubyMine was not able to run your tests, something like the following (Figure 3-4).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig4_HTML.jpg
Figure 3-4

RubyMine Cannot Run the Tests

In order to make this succeed, you need to follow the next steps:
  1. (1)
    While having your project in RubyMine, select “Run ➤ Edit Configurations…” (Figure 3-5).
    ../images/497830_1_En_3_Chapter/497830_1_En_3_Fig5_HTML.jpg
    Figure 3-5

    Edit Project Run Configurations

     
  2. (2)
    Then select “Templates” and “Test::Unit/Shoulda/Minitest”; make sure that the “Configuration” tab is selected and the Mode is “All tests in folder.” You will have to fill in the “Working directory” and some extra “Ruby arguments” (Figure 3-6).
    ../images/497830_1_En_3_Chapter/497830_1_En_3_Fig6_HTML.jpg
    Figure 3-6

    RubyMine Test Configuration Defaults

     
  3. (3)

    The “Working directory” needs to be filled in with the full path to your “test” folder. Click the folder button and navigate to the “test” folder of your project.

    The Ruby arguments that you need to add are -I. -r 'test_helper'.

    Hence, you need to have something like the following (Figure 3-7).
    ../images/497830_1_En_3_Chapter/497830_1_En_3_Fig7_HTML.jpg
    Figure 3-7

    Working Directory and Ruby Arguments

     
  4. (4)

    Then you click OK, and you are ready to repeat your try to run your tests.

     
Right-click the project name and select RunAll tests in string_combiner. Your tests should succeed (Figure 3-8).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig8_HTML.jpg
Figure 3-8

RubyMine Ran Tests Successfully

You can also run an individual file. Pick up one of the files that contain tests, and right-click. Then select “Run ➤ ‘Run test…’.”

Moreover, you can also run an individual test. In order to do that, you need to open the file with the tests in your editor area. Then you need to right-click inside the method body that corresponds to the test that you want to run, like the following (Figure 3-9).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig9_HTML.jpg
Figure 3-9

Selecting to Run an Individual Test

On the preceding screenshot, you can see that I have right-clicked inside the file test/test_customer.rb, in the body of the method test_has_public_last_name. And I am about to select to run the individual test. If I do that, RubyMine will execute the particular test and will print the result at the bottom tab.

But how does RubyMine report back to you a test that is failing? Let’s see that.

You will first change a test method implementation to make it fail. Go to file test/test_customer.rb and change the method implementation of the test test_has_public_last_name to be as follows:
def test_has_public_last_name
  customer = Customer.new('John', 'Papas')
  assert_equal 'Papas', customer.last_name.downcase
end
In other words, the original line
assert_equal 'Papas', customer.last_name
was changed to
assert_equal 'Papas', customer.last_name.downcase
Now, right-click your project and select to run all tests. When you do that, you will have the test test_has_public_last_name failing (Figure 3-10).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig10_HTML.jpg
Figure 3-10

RubyMine: One Test Has Failed

RubyMine is displaying the line at which the failing test failed. You can click this line in order to quickly jump to the failing line of test code in the editor (Figure 3-11).
../images/497830_1_En_3_Chapter/497830_1_En_3_Fig11_HTML.jpg
Figure 3-11

Click to Jump to the Failing Test Line

That was our first encounter with the RubyMine integration of minitest. Experiment with the keyboard shortcut and options if you want to improve your skills.

Minitest: Other Assert Commands

You have learned about the assert_equal and assert_nil commands that minitest offers to carry out assertions. These are very powerful, but minitest is not limited only to these two assertion commands.

All the minitest assertion methods can be found here: Minitest:​:​Assertions.

Look at the public instance methods. The API is very easy to understand and use.

Task Details

Write Tests Using Minitest
You will need to write minitest tests for the following class (Listing 3-22).
# File: sum_of_three.rb
#
class SumOfThree
  attr_accessor :array_of_integers
  def initialize(array_of_integers)
    @array_of_integers = array_of_integers
  end
  # Tries to find 3 integers that sum up to 0.
  # In other words given an array a, we want to find
  # the 3 integers that satisfy the equation:
  #
  #    a[i] + a[j] + a[k] == 0
  #
  # where i != j && i != k && j !=k
  #
  # The algorithm first sorts the input array, and then follows a clever algorithm that does not have to
  # use any search for pairs.
  #
  # This is the pseudo-algorithm
  #
  #  sort(array_of_integers);
  #
  #  for i = 0 to n - 3 do
  #    a = S[i];
  #    j = i+1;
  #    k = size_of_array - 1;
  #    while (j < k) do
  #      b = S[j];
  #      c = S[k];
  #      if (a + b + c == 0) then
  #        return a, b, c; # This is the happy case and we stop
  #      else if (a + b + c > 0) then
  #        k = k - 1; # In this case, the b + c is big enough and we need to make it smaller. We know for sure that c is quite big
  #                   # because it has been set as the value of the element that is on the far right, a.k.a. the biggest one.
  #                   # So, let us try to use the previous element, which is smaller than c. Hence we will make the (b+c) factor
  #                   # smaller and the (a + b + c) moving closer to 0.
  #      else
  #        j = j + 1; # In this case, the b + c is small enough so that the (a + b + c) < 0. We need to increase b + c but
  #                   # not so much to go over 0. We need to increase it a little bit. That's why we decide to pick up the
  #                   # next biggest element, which is j + 1.
  #      end
  #    end
  #  end
  #
  def find_three
    array_of_integers.sort!
    i = 0
    size_of_array = array_of_integers.size
    while i <= size_of_array - 3
      a = array_of_integers[i]
      j = i + 1
      k = size_of_array - 1
      while j < k
        b = array_of_integers[j]
        c = array_of_integers[k]
        sum = a + b + c
        if sum.zero?
          return [a, b, c]
        elsif sum.positive?
          k -= 1
        else
          j += 1
        end
      end
      i += 1
    end
    []
  end
end
Listing 3-22

Class to Write Tests For

As you can read from the comments, this class is initialized with an array of integers. Then one can call the instance method #find_three that returns back a subset of these integers. The subset is of size 3, that is, it contains three of the integers in the original array. The property of these three integers returned is that they sum up to 0:
  1. 1.

    Create a RubyMine project for that.

     
  2. 2.

    Write your tests using minitest.

     
  3. 3.

    Make sure that you use a test_helper.rb file.

     
  4. 4.

    Make sure that you use a Gemfile to install all the necessary gems.

     
  5. 5.

    Make sure that you can run your tests from the command line using the bundle exec rake command.

     
  6. 6.

    Write enough tests to make sure that your class works as expected.

     
  7. 7.

    Make sure that you cover the edge cases like empty array, array with less than three integers, and so on.

     

Key Takeaways

  • Minitest API

  • TDD with minitest

  • Writing minitest test classes

  • Running the whole test suite or an individual filename

  • Refactoring your tests to improve implementation

  • Using rake

  • Integrating minitest with RubyMine

In the following chapter, you will be introduced to RSpec.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.220.140.5