🧪
testing with RSpec
on programminglast 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:
- given two strings with matches, our return value should be an array of all matching substrings
- given two strings with no matching substrings, our return value should be
false
- the given strings won't contain any spaces or special characters
- casing doesn't matter
This was the simple structure we built out for our JavaScript solution and corresponding test cases.
Using a similar structure, we're going to solve that same problem but with Ruby and RSpec test coverage instead.
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
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.
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
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.