Here's a quick bite for those using result monads provided by dry-monads to code workflows and test them with Minitest . Nothing fancy, please leave suggestions to make it more useful in the comments.

As a quick reminder: Result monads are simple objects which wrap the return value of methods in a uniform manner thus turning them into building blocks which can be stacked on top of one another like Lego bricks. Operations in Hanami 2 make use of them thru dry-operations, a thin extra layer which adds steps to the mix. Together, these great gems can turn a bowl of messy if/then/else/rescue spaghetti into a well layered lasagne. You can eat both of them alright, but only the lasagne will earn you applause for the optics and the ability to add more layers ad lib.

You still want to test this though, so here are the custom Minitest (assertions and) expectations for the job:

# frozen_string_literal: true

require 'dry-monads'

module Minitest::Assertions
  def assert_success(expected, actual, msg=nil)
    msg = message(msg) { "Expected #{mu_pp(actual)} to be Success with #{mu_pp(expected)}" }
    assert(expected == actual.value!, msg)
  end

  def refute_success(expected, actual, msg=nil)
    msg = message(msg) { "Expected #{mu_pp(actual)} not to be Success with #{mu_pp(expected)}" }
    assert(expected != actual.value!, msg)
  end

  def assert_failure(expected, actual, msg=nil)
    msg = message(msg) { "Expected #{mu_pp(actual)} to be Failure with #{mu_pp(expected)}" }
    assert(expected == actual.failure, msg)
  end

  def refute_failure(expected, actual, msg=nil)
    msg = message(msg) { "Expected #{mu_pp(actual)} not to be Failure with #{mu_pp(expected)}" }
    assert(expected != actual.failure, msg)
  end
end

Dry::Monads::Success.infect_an_assertion :assert_success, :must_be_success
Dry::Monads::Success.infect_an_assertion :refute_success, :wont_be_success

Dry::Monads::Failure.infect_an_assertion :assert_failure, :must_be_failure
Dry::Monads::Failure.infect_an_assertion :refute_failure, :wont_be_failure

Say you want to test the following nonsensical example:

class Lasagne
  include Dry::Monads[:result]  

  def cooking_time(tool)
    case tool
      when :microwave then Success(10)
      when :oven then Success(30)
      when :nuke then Failure(:epic)
      else Failure(:unsupported_tool)
    end
  end
end

Here are the tests in spec notation (because I like it so much better than assertions):

describe Lasagne do
  subject { Lasagne.new }

  it "quickly cooks in a microwave (but don't tell any Italians)" do
    _(subject.cooking_time(:microwave)).must_be_success 10
  end

  it "cooks perfectly in an oven" do
    _(subject.cooking_time(:oven)).must_be_success 30
  end

  it "ends us all when using a :nuke" do
    _(subject.cooking_time(:nuke)).must_be_failure :epic
  end

  it "doesn't cook with just any tool" do
    _(subject.cooking_time(:nose_hair_trimmer)).must_be_failure :unsupported_tool
  end
end

You can of course do without, but I find the vanilla alternative less appealing:

it "cooks perfectly in an oven" do
  _(subject.cooking_time(:oven)).must_equal Dry::Monads::Success(30)
end

Bon appetit!

Banner image by Rainer Stropek

Author Of article : Sven Schwyn Read full article