DEV Community

loading...
CherryChain

TDD in an event driven application

ndr_brt
the machine plays me like an instrument
・3 min read

You (should) know, Test Driven Development is one of the greatest practice in software development: it gives you confidence in what you implement, it gives you courage to refactor or change behaviors, it forces you to think about what you are about to implement.

For sure not every piece of code deserves to be written test-first, but at least you should start thinking: “Can I write a test before?”.

Passing to an event driven world the process remains the same:
there’s a SUT, a trigger action, and some behavior verification:

Consider implementing a feature for an hypothetical Continuous Integration application, on tests executed should check the results and notify to the application. This application in based on Vert.x framework and is coded in Kotlin language.

So our first test will be: “on tests executed, when they passed, should emit tests passed”:

@ExtendWith(VertxExtension::class)
internal class CheckTestsExecutionTest {

  @Test
  @Timeout(3, timeUnit = SECONDS)
  internal fun `on tests executed, when they passed, should emit tests passed`(vertx: Vertx, context: VertxTestContext) {

    CheckTestsExecution(vertx.eventBus()).start()

    vertx.eventBus().consumer<JsonObject>("TestsPassed") {
      context.completeNow()
    }

    vertx.eventBus()
      .publish("TestsExecuted", JsonObject.mapFrom(
        TestsExecuted(listOf(
          TestResult("simple test", passed = true)
        )))
      )
  }
}
Enter fullscreen mode Exit fullscreen mode

There’s a lot of stuff in there. The VertxExtension *class is a junit5 extension provided by the library *vertx-junit5 that gives some facilities to write Vert.x unit tests.

But, the very important thing here is that there’s no assertion. The reason is simple: we are testing an asynchronous behavior, so we will get no synchronous output from our .

What we need here is to set an expectation. To do that we create a consumer on the event we expecting to be launched from our SUT. To make test pass then we call the completeNow method on context object (is a Vert.x junit5 extension utility to handle asynchronous tests, as well as @Timeout annotation).
The test will pass only when completeNow method is called before the timeout. Simple.

The last test row emits an event that should trigger our desired behavior.
Obviously, after fixing compilation errors, the test will fail. Red bar, cool.

Let’s write only enough code to make test pass:

class CheckTestsExecution(private val eventBus: EventBus) {

  fun start() {
    eventBus.consumer<JsonObject>("TestsExecuted") {
      eventBus.publish("TestsPassed", JsonObject())
    }
  } 
}
Enter fullscreen mode Exit fullscreen mode

Green bar. Go on.

Now, we need to describe the case where one test fails and so we will expect to receive TestsFailed event. We should also expect not to receive TestsPasses.

  @Test
  @Timeout(3, timeUnit = SECONDS)
  internal fun `on tests executed, when they failed, should emit tests passed`(vertx: Vertx, context: VertxTestContext) {
    CheckTestsExecution(vertx.eventBus()).start()

    vertx.eventBus().consumer<JsonObject>("TestsPassed") {
      context.failNow(RuntimeException("No TestPassed event should be emitted"))
    }

    vertx.eventBus().consumer<JsonObject>("TestsFailed") {
      context.completeNow()
    }

    val testResults = listOf(
      TestResult("simple test", passed = false)
    )
    vertx.eventBus()
      .publish("TestsExecuted", JsonObject.mapFrom(TestsExecuted(testResults)))
  }
Enter fullscreen mode Exit fullscreen mode

Red bar again, so, make it green ASAP!

class CheckTestsExecution(private val eventBus: EventBus) {

  fun start() {
    eventBus.consumer<JsonObject>("TestsExecuted") { message ->
      val testExecuted = message.body().mapTo(TestsExecuted::class.java)
      if (testExecuted.tests.none { !it.passed }) {
        eventBus.publish("TestsPassed", JsonObject())
      } else {
        eventBus.publish("TestsFailed", JsonObject())
      }
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

Perfect, you just implemented an event driven component with TDD on Vert.x in Kotlin. Refactoring is up to you.

As you see, no great difference from the classic TDD approach: think, write a test, do something and expect behaviors, just pay attention to asynchronous manners and use expectations.

Discussion (0)