After I got my sample working with the help of A hanami slice in a Rails app - #8 by cllns I ran into a testing issue, which makes me wonder whether I even understood the basic of how you’d use / test the result of an operation:
rm -rf hanami_sample2
hanami new hanami_sample2
cd hanami_sample2
mkdir -p lib/hanami_sample2/predictor
bundle exec hanami generate operation predictor.create_prediction
echo '
module HanamiSample2
module Predictor
class CreatePrediction < HanamiSample2::Operation
def call(input)
if input_valid?(input)
Success("Prediction created")
else
Failure("Invalid input")
end
end
private
def input_valid?(input)
!input.nil? && !input.empty?
end
end
end
end
' > app/predictor/create_prediction.rb
mkdir -p spec/predictor
echo '
require "spec_helper"
RSpec.describe HanamiSample2::Predictor::CreatePrediction do
let(:operation) { described_class.new }
describe "#call" do
context "when input is valid" do
it "returns a success result" do
result = operation.call("valid input")
# expect(result).to be_a Success
expect(result).to be_success
expect(result.value!).to eq("Prediction created")
end
end
context "when input is invalid" do
it "returns a failure result" do
result = operation.call(nil)
# expect(result).to be_a Failure
expect(result).to be_failure
expect(result.failure).to eq("Invalid input")
end
end
end
end
' > spec/predictor/create_prediction_spec.rb
bundle exec rspec
Given the spec support file for operations I was expecting to be able to use Success and Failure, but those names are not defined. Comment those two lines in to see that failure.
With the code as written above, I get:
1) HanamiSample2::Predictor::CreatePrediction#call when input is invalid returns a failure result
Failure/Error: expect(result).to be_failure
expected `Success(Failure("Invalid input")).failure?` to be truthy, got false
# ./spec/predictor/create_prediction_spec.rb:23:in `block (4 levels) in <top (required)>'
2) HanamiSample2::Predictor::CreatePrediction#call when input is valid returns a success result
Failure/Error: expect(result.value!).to eq("Prediction created")
expected: "Prediction created"
got: Success("Prediction created")
(compared using ==)
Diff:
@@ -1 +1 @@
-"Prediction created"
+Success("Prediction created")
# ./spec/predictor/create_prediction_spec.rb:14:in `block (4 levels) in <top (required)>'
I haven’t had a ton of time to look into it, but I’m stumped why config.include Dry::Monads[:result] in the operations spec support file isn’t working. Even directly including include Dry::Monads[:result] directly in the spec isn’t working
As for the other issue, you need to put step in front of the Success and Failure calls. This isn’t at all obvious though and we should make the ergonomics better. We should at least improve the documentation to talk about this gotcha. It’s quite weird to add step directly in front of Success and Failure constructors, so I’d re-work it to something like this:
module HanamiSample2
module Predictor
class CreatePrediction < HanamiSample2::Operation
def call(input)
step validate_input(input)
end
private
def validate_input(input)
if !input.nil? && !input.empty?
Success("Prediction created")
else
Failure("Invalid input")
end
end
end
end
end
By not adding step, it’s wrapping your result in Success. Instead of that, I think I’d prefer seeing a helpful error telling the user about step conventions. Additionally, this could be fixed at a lower level, by preventing all nested monads, e.g. Success(Failure(...)). It’s hard to reason about monads in a dynamic type system, so I feel like this is a reasonable trade-off to make it simpler for users.
Thank you, @cllns! This allows me to fully implement my bigger example… ah, not quite. I will, once again switch to a new topic, Rails and hanami-view don't get along, to discuss a different issue.
I’ve been dealing with this annoyance for a long time. Here’s the thing: it does give you access to those class constants, but RSpec is a special case because everything is written in blocks, and so the scope of constant lookup is actually the global scope, and not the ExampleGroup class.
I’m not certain that adding these constants to the global scope is desirable. Some ways I work around it: M alias for Dry::Monads so I can ref M::Success etc, and success_array and failure_array helpers.
Ahhh yes, I see. That makes sense @alassek, thanks for the insight.
Wouldn’t this work as well as our operation spec support file, to get the short-hand Success and Failure constants only for specs? So we don’t pollute the global namespace.
# frozen_string_literal: true
require "dry/monads"
RSpec.configure do |config|
# Provide `Success` and `Failure` for testing operation results
config.include Dry::Monads[:result]
config.before(:all) do
Success = Dry::Monads::Success
Failure = Dry::Monads::Failure
end
end
(Dry::Monads::Success is an alias for Dry::Monads::Result::Success)
Here’s a reason why I don’t like adding these to the global scope: if you are referencing one of them in a place where normally those constants wouldn’t exist, you won’t see this bug in your test suite, only in production.
I think if you’re going to do this, it needs to go all the way. You need to define these globals for the whole app in production as well.
I don’t think it’s possible to have it both ways. This is a consequence of RSpec’s DSL being built around closures; something like Minitest wouldn’t have this problem.
Ah of course. I was thinking it would just define it within RSpec’s context, but it does pollute the global namespace for app code too, and I agree that’s a bad idea. I once got bit by something similar (using FactoryBot, which was adding a monkey patch only in test, but not production).