Railway-oriented business transactions are a great way to unclutter your Rails controllers. We’ve already seen how to write ‘em. Now let’s see how we can test ‘em. I’ll be using dry-transaction as a business transaction DSL and RSpec for testing. I’m assuming that you already have RSpec set up for your app. If not, check Aaron Sumner’s blog and his book Everyday Rails Testing With RSpec to saddle up.
Starting point: a basic transaction
Here’s a basic transaction. I’ll fetch some params and create a new lead. Based on the database response, I’ll return either a Success or a Failure.
Now, I want to test it. So where do I start?
Testing transactions: the basics
First, I’ll create a test file.
Then, I’ll write our test bare bones.
What I’m doing here:
described_class.call(params: params) is calling my transaction with the awaited input (params)
subject is a special variable that refers to my transaction being tested
let(:params) stores the hash I’m sending to my transaction
Covering positive scenarios
What should I test then? The first thing I want to test is the transaction returning a Success when called with valid params. From this starting point, I can check if the transaction does create a Lead or if this lead has the proper attributes filled in.
what’s happening to my database when the transaction is called (with expect { subject }.to())
if my transaction returns a Success and the content of this Success (with subject.success)
the newly created Lead’s attributes
Testing for successes and failures
Say I want to send a SMS to my new lead through an external service. Let’s add a step to my transaction.
Easy right? But MySmsProvider only provides production-ready credentials. So how can I test MySmsProvider’s different responses? RSpec allows me to mock responses.
Let’s start with mocking a positive response. Our basic assumptions shouldn’t change because whatever steps we’re adding to the transaction, it should always return a Success or a Failure.
But what if we want to test a negative response (i.e: Steven’s phone number is invalid)?
I can either add a context block with specific params or mock a error message from MySmsProvider. I’ll do the former. context behaves like a sub-folder where I can inherit my test’s top-level information yet change it if need be.
In the last context 'a new lead signed up with invalid params', the before do block defined in the first context does not apply (it’s context dependent). I’ve decided to trigger an error based on my input rather than mocking an error with allow_any_instance_of. This way, I can decide to enforce validations at the transaction level to return a Failure if some data are missing.