Expectations with RFC 2119 compliance

Cyril Kato
3 min readSep 12, 2015

--

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")
end
task 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!

--

--