We’re finally back to our TDD series. Last time, we successfully actuated our message handler.
Today, we check to see if our handler does anything. We left our test in the following state:
context "Initiated" do
handler = Handlers::Commands.new
build = Messages::Commands::Build.new
handler.(build)
end
From our event flow diagram, we know that a Build
command is supposed to result in an Initiated
event. So our first step will be to check if we did indeed write such an event. To do that, we could imagine that our handler
has some collaborator that is capable of writing messages to a message store:
# Within the context "Initiated" block
handler.(build)
writer = handler.write
(In the interest of saving email space, I’ll try to not reproduce reams of duplicated code. If you aren’t able to follow the thread of the code, please let me know.)
write
seems like a reasonable name for such a collaborator. Let’s try running the test:
$ ruby test/automated/handle_commands/build/initiated.rb
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [arm64-darwin21]
TEST_BENCH_DETAIL: nil
Handle Commands
Build
Initiated
test/automated/handle_commands/build/initiated.rb:19:in `block (3 levels) in <main>': undefined method `write' for #<CompositeComponent::Handlers::Commands:0x0000000103e5b678...> (NoMethodError)
handler
has no method write
. So the test is telling us we need to add it.
Let’s open the handler class at lib/composite-component/handlers/commands.rb
and add it:
module CompositeComponent
module Handlers
class Commands
include Messaging::Handle
dependency :write, Messaging::Postgres::Write
end
end
end
The dependency
method comes from the corresponding Eventide gem, and our 2nd ever Utah Microservices Meetup culminated in demonstrating how it works. For now we’ll just say that it adds a method write
because that’s the symbol we passed to it. Because we also passed Messaging::Postgres::Write
to it, whatever write
returns will have the same interface as Messaging::Postgres::Write
and give us errors if we violate that interface.
If we ran the test at this point, it would pass, so let’s go back to the test and leverage write
:
# Within the context "Initiated" block
handler.(build)
write = handler.write
initiated = write.one_message do |event|
event.instance_of?(Messages::Events::Initiated)
end
If we’re going find an Initiated
event, we might as well name it initiated
. But what is this new write
devilry?
dependency
does one other thing for us. If the thing we pass as interface, again in our case Messaging::Postgres::Write
declares a Substitute
module, then dependency
will actually use that as the return value of the method it adds. Our interface does, and you can read it’s full interface and watch the latest Utah Microservices Meetup which went deep into how this substitute works.
In short though, this substitute provides a one_message
method, and if we pass that method a block, it will call that block for each message written. It’s important to note that this substitute isn’t actually putting messages into Message DB. Everything written is in memory and only for the duration of the test.
If our block doesn’t ever return true
, which could mean either no messages were written or none were written for which the block returns true
, then one_message
will return nil
. If exactly one message returns true
, then one_message
will return that message. However, if more than one message returns true
, one_message
will raise an exception.
The content of our block checks to see a message is one of our Initiated
events. You might expect the test to give us a NameError
since we haven’t defined that message class yet, but it won’t. Let’s write our first assertion, and then see if you can figure out why:
# Within the context "Initiated" block
handler.(build)
write = handler.write
initiated = write.one_message do |event|
event.instance_of?(Messages::Events::Initiated)
end
test "Wrote an Initiated event" do
refute(initiated.nil?)
end
In TestBench, it’s good practice to wrap assertions in a test
block. Within that block we refute
that initiated
is nil
. refute
is the same as assert(!<whatever>)
. Remember the three results we could have from calling write.one_message
? We want to make sure it found a message.
That refutation will fail, but we won’t get that NameError
(think about why):
$ ruby test/automated/handle_commands/build/initiated.rb
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [arm64-darwin21]
TEST_BENCH_DETAIL: nil
Handle Commands
Build
Initiated
Wrote an Initiated event
test/automated/handle_commands/build/initiated.rb:26:in `block (4 levels) in <main>': Assertion failed (TestBench::Fixture::AssertionFailure)
Your line number of where the refutation failed may not match mine, so don’t be alarmed by that, but it will point to the line where you have the refutation. Do you remember the two reasons why the write.one_message
call might return nil
? One was that none of the messages written matched the block, and the other was that no messages were written. We’re in this case, which makes sense since if we looked at the handler code, we’d find no evidence of it doing anything yet. As a result of no messages being written, the content of our block never executes.
We’ll fix that in the next one though because this one is getting long.