TDD in Eventide: Getting assertive

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 writebecause 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.


Like this message? I send out a short email each day to help software development leaders build organizations the deliver value. Join us!


Get the book!

Ready to learn how to build an autonomous, event-sourced microservices-based system? Practical Microservices is the hands-on guidance you've been looking for.

Roll up your sleeves and get ready to build Video Tutorials, the next-gen web-based learning platform. You'll build it as a collection of loosely-coupled autonomous services, developing a message store interface along the way.

When you're done, you'll be ready to contribute to microservices-based projects.

In ebook or in print.