Testing Ruby solutions

We use minitest as a testing framework for Ruby coding exercises.

Hello, World!

Before we go into details, let's see how you can use minitest to test the most basic Ruby task: writing a function that returns "Hello, World!".

This is what a student might write (hello.rb):

def hello_message
    "Hello, World!"
end

And this is an example of what you could write to evaluate the student's code:

require "minitest/autorun"
require "./hello"

class Evaluate < Minitest::Test
    def test_hello_message
        assert_equal "Hello, World!",
                     hello_message,
                     "Your function should return: Hello, World!"
    end
end

Let's examine the above evaluation code.

We first need to require the minitest framework and the user file(s).

Minitest::Test

Then we need to define a suite of unit tests as a class that inherits from Minitest::Test.

This groups together multiple unit tests and allows implementing common functionality that you want executed before and after each test, via the setup() and teardown() methods respectively.

Unit tests can be implemented as methods inside this class that start with test_. These are the only functions that will be executed when running the evaluation code.

class Evaluate < Minitest::Test
    def setup
        # This code will be executed before every unit test.
    end

    def teardown
        # This code will be executed after every unit test.
    end

    def test_something
        # This is a unit test.
    end

    def test_sometings_else
        # This is another unit test.
    end
end

Assertions

Assertions are the statements that actually verify that the student's code behaves as expected. The most common pattern is to call student's methods and assert that their return value is the one that you expect. If it is not, then the test will fail and you have the opportunity to provide a custom message to the student for each failed assertion, so that she can improve.

These are the most common ones:

assert(boolean_value, msg = nil)
assert_equal(expected_value, actual_value, msg = nil)

The assertion from the Hello World example could be rewritten as:

assert "Hello, World!" == hello_message,
       "Your function should return: Hello, World!"

If any of the assertions in your unit tests fail, the test is marked as failed and feedback (the msg parameter) is given to the student.

Standard output

In the previous example we had the students return Hello, World!, but actually a more common task is to have them print the same string to the standard output.

def say_hello
    puts "Hello, World!"
end

How do we test this specific function?

require "minitest/autorun"
require "./hello"

class Evaluate < Minitest::Test
    def test_hello_message
        out, err = capture_io do
            say_hello
        end

        assert_equal "Hello, World!\n", out, "Your function should return: Hello, World!"
    end
end

Minitest provides the capture_io method which captures and returns standard output and standard error for all the statements that are executed within the method's block.

Technical specs

  • Ruby >= 2.3.1
  • Minitest >= 5.9.0

Example 1: FizzBuzz

Given:

  • An integer number n

Your task is to:

  • Write a function that returns an array with the numbers from 1 to n with the following restrictions:
    • for multiples of 3 store "Fizz" instead of the number
    • for multiples of 5 store "Buzz" instead of the number
    • for numbers which are multiples of both 3 and 5 store "FizzBuzz"

Example:

fizzbuzz(15) == [
    1, 2, "Fizz", 4, "Buzz",
    "Fizz", 7, 8, "Fizz", "Buzz",
    11, "Fizz", 13, 14, "FizzBuzz"
]

Here's how a student might solve the problem:

def fizzbuzz(n)
    result = []

    (1..n).each do |i|
        if i % 15 == 0
            result.push "FizzBuzz"
        elsif i % 3 == 0
            result.push "Fizz"
        elsif i % 5 == 0
            result.push "Buzz"
        else
            result.push i
        end
    end

    result
end

Here's how you might test the student's solution:

require "minitest/autorun"
require "./fizzbuzz"

class Evaluate < Minitest::Test
    def test_fizzbuzz_simple
        assert_equal [1], fizzbuzz(1), "fizzbuzz(1) should return [1]"
    end

    def test_fizzbuzz
        result = fizzbuzz(15)

        assert result.kind_of?(Array), "Your function should return an array"

        expected = [
            1, 2, "Fizz", 4, "Buzz",
            "Fizz", 7, 8, "Fizz", "Buzz",
            11, "Fizz", 13, 14, "FizzBuzz"
        ]

        assert_equal expected.length,
                     result.length,
                     "Your function should return #{expected.length} objects"

        result.each_with_index do |obj, idx|
            assert_equal expected[idx],
                         obj,
                         "Your function should map #{idx + 1} to #{expected[idx]} instead of #{obj}"
        end
    end
end

Example 2: Title Case

Given:

  • A sentence with words separated by spaces

Your task is to:

  • Write a function that returns a copy of this sentence where all the words:
    • start with a capital letter
    • the rest of the letters are lower case

Note:

  • Your function should not modify the sentence given as argument.

Example:

normalize("This is SO  MUCH  FUN! ") == "This Is So  Much  Fun! "

Here's how a student might solve the problem:

def normalize(sentence)
    copy = sentence.dup
    idx = 0

    while idx < copy.length
        idx = skip_whitespace copy, idx
        idx = normalize_word! copy, idx
    end

    copy
end

def skip_whitespace(sentence, idx)
    while idx < sentence.length and sentence[idx] == ' '
        idx += 1
    end

    idx
end

def normalize_word!(sentence, idx)
    if idx < sentence.length and sentence[idx] != ' '
        sentence[idx] = sentence[idx].upcase
        idx += 1
    end

    while idx < sentence.length and sentence[idx] != ' '
        sentence[idx] = sentence[idx].downcase
        idx += 1
    end

    idx
end

Here's how you might test the student's solution:

require "minitest/autorun"
require "./normalize"

class Evaluate < Minitest::Test
    def test_character
        assert_equal "A", normalize("a"), "Your function should convert a single character"
        assert_equal "A", normalize("A"), "Your function should convert a single character"
    end

    def test_no_mutate
        sentence = "a"
        normalize sentence
        assert_equal "a", sentence, "Your function should not mutate the argument"
    end

    def test_word
        assert_equal "Abc", normalize("abc"), "Your function should convert a single word"
        assert_equal "Abc", normalize("ABC"), "Your function should convert a single word"
        assert_equal "Abc", normalize("aBC"), "Your function should convert a single word"
        assert_equal "Abc", normalize("Abc"), "Your function should convert a single word"
    end

    def test_sentence
        assert_equal "Abc Def Ghi",
                     normalize("abc def ghi"),
                     "Your function should convert a sentence"
    end

    def test_multiple_whitespace
        assert_equal "  Abc  Def Ghi ",
                     normalize("  abc  def ghi "),
                     "Your function should keep multiple whitespaces"
    end
end

results matching ""

    No results matching ""