Expectations with RFC 2119 compliance
In the Ruby community, the two main testing frameworks offer different syntaxes to check the code:
- In RSpec, until recently, both expect and should syntaxes was available. For some technical reasons, however, the should syntax is not recommended.
- In minitest, expectations can be expressed through the Ruby or in an RSpec style. In the second case, a “#test_*” method would be dynamically defined per spec in order to be translated into a test. By the way, spec matchers are aliased by test matchers.
So we have several different syntaxes, but only one same level of requirement: absolute, which would correspond to MUST keyword in the RFCs.
However, as human beings, we would not write RFCs with a single level of requirement. Instead, we would prefer to qualify expectations, using the additional keywords “SHOULD” and “MAY” as defined in RFC 2119.
That’s precisely what Spectus is made for.
Enter Spectus!
Spectus is a very small and well focused Ruby gem. It can be installed with:
gem install spectus
What Does Spectus Look Like?
Various combinations can occur between the requirement levels and the code that could be evaluated as:
- Implemented & Matched
- Implemented & Not matched
- Implemented & Exception
- Not implemented
As expectation is an assertion that is either true or false, Spectus results can respectively pass or fail.
To see a little clearer, let’s take a look at some examples… Because Spectus is basically a module defining methods that can be used to qualify expectations in specifications, for convenience we will also instantiate some matchers from the Matchi library:
gem install matchi
Let’s now make Spectus available:
require "spectus"
require "matchi/helper"include Matchi::Helper
We are now ready to experiment!
Absolute Requirement expectation
There is exactly one bat:
definition = Spectus.must equal 1
definition.call { "🦇".size }
# => Expresenter::Pass(actual: 1, error: nil, expected: 1, got: true, matcher: :equal, negate: false, level: :MUST
The test is passed.
Absolute Prohibition expectation
Truth and lies:
definition = Spectus.must_not be_true
definition.call { false }
# => Expresenter::Pass(actual: false, error: nil, expected: nil, got: true, matcher: :be_true, negate: true, level: :MUST
Recommended expectation
A well-known joke. The addition of 0.1
and 0.2
is deadly precise:
definition = Spectus.should equal 0.3
definition.call { 0.1 + 0.2 }
# => Expresenter::Pass(actual: 0.30000000000000004, error: nil, expected: 0.3, got: false, matcher: :equal, negate: false, level: :SHOULD
Not Recommended expectation
This should not be wrong:
definition = Spectus.should_not match "123456"
definition.call do
require "securerandom"
SecureRandom.hex(3)
end
# => Expresenter::Pass(actual: "ce22e3", error: nil, expected: "123456", got: true, matcher: :match, negate: true, level: :SHOULD
In any case, as long as there are no exceptions, the test passes.
Optional expectation
An empty array is blank, right?
definition = Spectus.may be_true
definition.call { [].blank? }
# => Expresenter::Pass(actual: nil, error: #<NoMethodError: undefined method `blank?' for []:Array>, expected: nil, got: nil, matcher: :be_true, negate: false, level: :MAY
My bad! ActiveSupport was not imported. 🤦♂️
Anyways, the test passes because the exception produced is NoMethodError
, meaning that the functionality is not implemented.
Spectus’s Matchers
Because Spectus relies on Matchi, a collection of expectation matchers is available:
- eql: equivalence matcher
- equal: identity matcher
- match: regular expressions matcher
- raise_exception: expecting errors matcher
- be_true: truth matcher
- be_false: untruth matcher
- be_nil: nil matcher
- be_an_instance_of: type/class matcher
Note: If you look at the source for matchi, you’ll see that each matcher is mainly a class responding to “#matches?” method.
Running Spectus Specs In Your Tests
Generally, running Spectus tests can use the same mechanisms as you would for RSpec or minitest tests, so there’s not much to do:
require "spectus"
require "rake/testtask"Rake::TestTask.new do |t|
t.pattern = File.join("test", "**", "*.rb")
endtask default: :test
Give It A Try
So next time you’re starting on a new small and well focused Ruby project, before requiring RSpec or minitest, give Spectus a try!
Thanks for reading!