« Back

🧪

testing with RSpec

on programming
last updated 12/10/20

Writing test coverage is the programming equivalent of eating vegetables. We usually don't want to but know that we should.

Let's pick up where we left off in the previous post. As a refresher, our problem was to find all matching substrings between any two given strings. We also made a few assumptions:

This was the simple structure we built out for our JavaScript solution and corresponding test cases.

Jest test output

Using a similar structure, we're going to solve that same problem but with Ruby and RSpec test coverage instead.

RSpec test output

Like Jest is for JavaScript, RSpec is a popular testing framework for applications written in Ruby. To get started, we'll run rspec --init from our top-level directory.


cd practice
rspec --init
        

This generates a spec directory containing a spec_helper.rb file — neither of which we need to touch. More importantly, we also now have a .rspec file to configure our test settings. Let's toggle RSpec's output format because the default setting doesn't offer much context.


# practice/.rspec
--require spec_helper # this is included by default (which we need)
--format documentation # adding this to display test descriptions upon execution
        
RSpec tests without context
This is our test output if we don't include --format documentation in .rspec

Now we're able to start writing our tests. We can copy over test descriptions from our JavaScript directory, but we'll obviously need to tweak the syntax.


mkdir matched-substrings-rb
cd matched-substrings-rb
touch matched_substrings.rb # a file for solving our problem
touch matched_substrings_spec.rb # a file to test that solution

# practice/matched-substrings-rb/matched_substrings_spec.rb
require_relative '../spec/spec_helper.rb'
require_relative './matched_substrings.rb'

describe 'match substrings' do
  test = Solution.new

  it 'takes two strings and returns all matching substrings' do
    expect(test.match('te', 'test')).to eq ['t', 'te', 'e']
  end

  it "order of strings passed should not matter" do
    expect(test.match('test', 'te')).to eq ['t', 'te', 'e']
  end

  it "returns case-insensitive matches" do
    expect(test.match('TE', 'test')).to eq(['t', 'te', 'e'])
  end

  it "returns false when given strings with no matching substrings" do
    expect(test.match('no', 'match')).to eq false
  end
end
        

Those first two lines are pretty straightforward: we're just loading a helper for our tests and then the file with our solution code.

The describe method nests related tests in an 'example group'. Whatever argument we pass to describe will ultimately appear as a header in our terminal upon test execution.

RSpec test output using 'describe'
a quick note: describe 'match substrings' do is equivalent to describe('match substrings') do

This brings us to our solution code. Performance and elegance aside, we have a simple Solution class defined with a #match method that will handle the substring matching process.


# practice/matched-substrings-rb/matched_substrings.rb
class Solution
  def match(str1, str2)
    matches = []
    s1 = str1.downcase
    s2 = str2.downcase

# iterate over each substring in the first string
  # ex. given "test":
  #   first outer loop: "t", "te", "tes", "test"
  #   second outer loop: "e", "es", "est"...
# if the 2nd string doesn't include the substring and the substring isn't in the array yet, add it
    (0...s1.length).each do |i|
      (i...s1.length).each do |j|
        substring = s1[i..j]
        if s2.include?(substring) && !matches.include?(substring)
          matches << substring
        end
      end
    end

    return !matches.empty? ? matches : false
  end
end
        

Because we've loaded this solution file back in our test file, we can instantiate new instances of this Solution class and invoke its #match method for test cases.


require_relative '../spec/spec_helper.rb'
require_relative './matched_substrings.rb' # loading our solution file

describe 'match substrings' do
  test = Solution.new # instantiating a new Solution instance to use its #match method
    ...
end
        

Similar to describe, the argument we pass to it will display in our terminal as context for the test contained in its subsequent block.

For that block, we're calling expect() and then to() on that result, finally using eq to check whether their return values are equivalent. eq is RSpec's version of ==.


test = Solution.new

it 'takes two strings and returns all matching substrings' do
  expect(test.match('te', 'test')).to eq ['t', 'te', 'e']
end

# expect().to()

# expect()
# passing expect() an argument of...
# an instance of Solution and calling #match, passing two strings, on that instance
expect(test.match('te', 'test'))

# .to()
.to eq(['t', 'te', 'e'])
        

Again, describe is for describing what we're testing and it is for explaining what those results should be. expect and to are both methods that we can use with RSpec's matchers — eq is just one of them — in comparing actual results to expectations.

After following a similar process for our other three test cases, we can run them all from the top-level practice directory.


# rspec <directory>/<file_with_test_cases>
rspec matched-substrings-rb/matched_substrings_spec.rb
        
failing test cases
red for failing test cases
passing test cases
green means we're all good

RSpec is capable of way more than what we're using it for here so whether you want to review these basics or go deeper, their documentation can help. Minitest, Capybara, and Cucumber are all viable alternatives too.

Thanks for reading and again, feel free to use my repo as a point of reference.