TDD in Eventide: Making the handler do something

(Reminder that the completed component code is on GitLab. The completed state is different from what we’re working through here, but the files are at least all in pplace.)

In our last installment, we finished with a test that will tell us if our handler is writing the correct type of message as part of its execution.

The handler currently isn’t. In fact, it isn’t writing any message at all. Let’s change that. Back to lib/composite_component/handlers/commands.rb, where we’ll see a handler come into being. Our first step is to declare that this handler actually handles something:

# Leaving out the module lines to save some space
class Commands
  include Messaging::Handle
  include Messages::Commands # <- Added the commands

  dependency :write, Messaging::Postgres::Write

  handle Build do |build| # <- The Build command handler
  end
end

This adds a handle call to the class definition. When we included Messaging::Handle, we picked up this handle method that lets us declare that we handle a particular message type. In this case, it’s the Build command. We named this class Commands, so it stands to reason that the message we handle in here would be commands.

We also include Messages::Commands. This makes it so that we don’t have write handle Messages::Commands::Build. I’m not one to reduce keystrokes just to reduce keystrokes because chasing that end sometimes makes things less clear. I don’t think this makes things less clear.

A handle call takes both the message class we’re handling and a block that implements what we do when handling that class’s message type. That block’s only parameter is an instance of that message class, and we typically name that variable the snake_case version of the class’s name.

The test assertion will still fail at this point because our handler still isn’t doing anything. Remember that we’re writing the simplest code that makes the test pass:

class Commands
  include Messaging::Handle
  include Messages::Commands

  dependency :write, Messaging::Postgres::Write

  handle Build do |build|
    initiated = Initiated.new
  end
end

The simplest way to have a handler write a particular message is to just instantiate a message of that type. However, this time we’ll get that NameError we didn’t get last time. This code will actually execute:

$ 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
      /Users/ethangarofolo/source/such-software/utah-microservices-meetup/do-expensive-things-once/composite-component/lib/composite_component/handlers/commands.rb:31:in `block in <class:Commands>': uninitialized constant CompositeComponent::Handlers::Commands::Initiated (NameError)

If you read the error message carefully, you’ll notice the uninitialized constant CompositeComponent::Handlers::Commands::Initiated. It’s looking for an Initiated constant under Handlers::Commands. That’s because the class definition we’ve written doesn’t have that constant and neither does either module that we include. We haven’t written this message definition.

Let’s make a new file in lib/composite_component/messages/events/initiated.rb:

module CompositeComponent
  module Messages
    module Events
      class Initiated
      end
    end
  end
end

That structure mimics the Build command’s structure, so no surprises.

We need to get this loaded into our program, so we go to the component’s loader file at lib/composite_component.rb and add it:

require "eventide/postgres"

require "composite_component/messages/commands/build"

require "composite_component/messages/events/initiated" # <- Add it here

require "composite_component/handlers/commands"

The test would still fail at this point with the same error because we haven’t included our events into the handler class. Let’s fix that back in lib/composite_component/handlers/commands.rb:

class Commands
  include Messaging::Handle
  include Messages::Commands
  include Messages::Events

  dependency :write, Messaging::Postgres::Write

  handle Build do |build|
    initiated = Initiated.new
  end
end

Run the tests, and you’ll see we’re back to the assertion failing because we’re still not writing the event. Let’s write it:

dependency :write, Messaging::Postgres::Write

handle Build do |build|
  initiated = Initiated.new

  write.(initiated, "somestream")
end

We make use of the write dependency to actually write this message. It takes first the message we’re going to write and then the name of the stream we’re writing it to.

If “somestream” seems odd to you as a stream name, good, you’re developing that sensibility. Remember that we’re writing the simplest code possible to get the test to pass. Our test currently only cares whether or not we’re writing an Initiated event. It doesn’t yet require us to write it to the correct place.

Let’s try running the test:

$ ruby test/automated/handle_commands/build/initiated.rb                                                                                                                                                                              [0/0]
ruby 3.2.1 (2023-02-08 revision 31819e82c8) [arm64-darwin21]

TEST_BENCH_DETAIL: nil

Handle Commands
  Build
    Initiated
      /Users/ethangarofolo/source/such-software/utah-microservices-meetup/do-expensive-things-once/composite-component/gems/ruby/3.2.0/gems/evt-transform-2.0.0.0/lib/transform/transform.rb:12:in `transformer_reflection': CompositeComponent::Messages::Events::Initiated doesn't have a Transform namespace that implements the transformation protocol (Transform::Error)

CompositeComponent::Messages::Events::Initiated doesn't have a Transform namespace that implements the transformation protocol. Lets you know that you haven’t implemented the transformation protocol. Okay, see you tomorrow!

As you get more familiar with the tools that Eventide offers, this error message will make deeper sense. It isn’t quite at “monads are monoids in the category of endofunctors” level, but we don’t need to dive into the transformation protocol right now. It is leveraged to serialize and deserialize messages to/from Message DB.

If we revisit our message class, we’ll see that it’s just a barebones Ruby class. We’re missing a critical line. Let’s add it in lib/composite_component/messages/events/initiated.rb:

module CompositeComponent
  module Messages
    module Events
      class Initiated
        include Messaging::Message # <- I'm a real message!
      end
    end
  end
end

Now our class is a message class. Consequently, if we run 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
      Wrote an Initiated event

It comes up all green. Or at least it did in my console. That’ll probably get lost over email. Congrats, you have a handler that writes a message. To some nonsensical stream, to be sure, but it writes one nonetheless.

Getting it to go to the right place is our next step.


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.