Have you ever had a business case specification that can be written as a set of steps? They are usually formed this way. On each step, there is an option to fail some validation or quit because of some other condition. This means the entire process has failed.
I've seen it many times handled as a list of functions with early returns on any unmet condition (or worse, nested
def user_sign_up(params) user = User.new(params) return :invalid_user unless user.valid? do_stuff(user) return :user_too_weird if user.too_weird? user end
Steps are usually non-obvious and there is a lot of branching to catch them all in tests. Methods get long, bugs happen. Sometimes an
Exception is thrown for a situation that is expected (like failed validation).
This is where SolidUseCase gets handy.
It works by defining a set of steps that will either continue or fail.
class UserSignUp include SolidUseCase steps :validate, :check_weirdness, :do_something_else end
Each of those steps needs to be implemented and can return
Failure with params. They will be called in order if the previous step returned
#run params will be passed to the first step. If you need more than one it's best to use a hash.
def validate(params) user = User.new(params) # note - fail won't return on its own return fail :invalid_user unless user.valid? # you can pass any object to the next method on continue, # including the same hash with more items continue user end def check_weirdness(user) do_stuff(user) return fail :user_too_weird if user.too_weird? continue user end
Invoking your use case
You can match the result of a use case.
UserSignUp.run(params).match do # Note: there can be only one success... success do |user| do_more_stuff(user) end # ...but many failure reasons... failure(:invalid_user) do |error_data| handle_errors(error_data, 'Oops, fix your mistakes and try again') end # ...with different params failure(:user_too_weird) do |error_data| handle_errors(error_data, 'It\'s not your fault, it\'s me') end
A full example can be found on GitHub. Since use cases usually wrap around business logic on any non-trivial process they are a good fit for functional tests. They have some limitations though. Use Cases do not for example support branching of successful steps.
We need to go deeper
Now for the stuff they don't show you in docs.
Remember that you have unified
#fail for all the steps? That allows you to nest Use Cases. Replace one step with another Use Case class name and that use case will run with your passed params.
class UserSignUp include SolidUseCase steps ValidateUser, # <- Class name :check_weirdness, SendNewUserWelcomeEmail, # <- Another one :do_something_else end
Note: design your Use Cases to ignore extra params in case you want to use them later as nested Use Cases.
Testing, testing, testing…
SolidUseCase library provides RSpec matchers.
describe UserSignUp do it 'runs successfully' do result = UserSignUp.new.run(username: 'alice', password: '123123') expect(result).to be_a_success end it 'fails when password is too short' do result = UserSignUp.new.run(username: 'alice', password: '1') expect(result).to fail_with(:invalid_user) end end
Next time when you have a set of steps to implement give them a try!
Subscribe to Chris Hasinski
Get the latest posts delivered right to your inbox