Skip to content

Testing

In general, Baker applications are easy to test because the Baker model enforces separation and decoupling between parts of your system. It provides a clear distinction between business logic (the recipe) and implementation details (interaction implementations). Furthermore, interactions are independent of each other, since every interaction only depends on its inputs.

This section describes testing strategies for the different layers of a Baker application. And demonstrates how to simplify testing by using the baker-test library.

Recipe validation tests

A recipe is validated when it's compiled by the RecipeCompiler. Any validation errors that might occur during this compilation are available through the resulting CompiledRecipe instance. A simple unit test that checks for validation errors in your recipe is essential.

package examples.java.recipes;

import com.ing.baker.compiler.RecipeCompiler;
import org.junit.Test;

import static org.junit.Assert.assertTrue;

public class WebShopRecipeTest {

    @Test
    public void recipeShouldCompileWithoutValidationErrors() {
        var validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).getValidationErrors();
        assertTrue(
            String.format("Recipe compilation resulted in validation errors: \n%s", validationErrors),
            validationErrors.isEmpty()
        );
    }
}
package examples.kotlin.recipes

import com.ing.baker.compiler.RecipeCompiler
import com.ing.baker.recipe.kotlindsl.ExperimentalDsl
import org.junit.Test

@ExperimentalDsl
class WebShopRecipeTest {
    @Test
    fun `recipe should compile without validation errors`() {
        val validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).validationErrors
        assert(validationErrors.isEmpty()) {
            "Recipe compilation resulted in validation errors: \n${validationErrors.joinToString(separator = "\n")}"
        }
    }
}
package examples.scala.recipes

import com.ing.baker.compiler.RecipeCompiler
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class WebShopRecipeTest extends AnyFlatSpec with Matchers {
  "Recipe" should "compile without validation errors" in {
    val validationErrors = RecipeCompiler.compileRecipe(WebShopRecipe.recipe).validationErrors
    assert(validationErrors.isEmpty)
  }
}

Business logic tests

The next layer is to test that the recipe actually does what you expect it to do at the business logic level. You can achieve this providing dummy or mock interactions that behave in an expected manner. The objective here is to test that the Recipe ends on the expected state given a certain order of firing sensory events.

The full test flow looks like this:

  1. Setup dummy or mock interactions that behave according to the test scenario.
  2. Create a new in memory Baker with the interactions from step 1.
  3. Add and bake the recipe
  4. Fire sensory events
  5. Assert expectations by querying the state of the recipe.

Implementation tests

The final layer is to individually test your implementations, which will resemble your normal e2e tests, interconnectivity tests, or unit tests. What these tests look like will strongly depend on your teams testing and coding conventions.

Baker test library

The baker-test library simplifies the testing of baker-based logic. Using this library makes the test code concise and readable in both Java and Scala. It also simplifies the testing of the cases when asynchronous recipe execution is involved.

Warning

At the moment baker-test is not compatible with the suspending Kotlin Baker APIs.

Adding the dependency

<dependency>
  <groupId>com.ing.baker</groupId>
  <artifactId>baker-test_2.13</artifactId>
  <version>${baker.version}</version>
  <scope>test</scope>
</dependency>
libraryDependencies += "com.ing.baker" %% "baker-test_2.13" % bakerVersion

EventsFlow

EventsFlow is made to simplify the work with the baker events while testing. EventsFlow is immutable.

You create a new events flow from events classes:

val flow: EventsFlow = 
      classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow
EventsFlow flow = EventsFlow.of(
    SomeSensoryEvent.class,
    InteractionSucceeded.class
);

There is also an option to create a new event flow from the existing one:

val anotherFlow: EventsFlow = 
    flow -- classOf[SomeSensoryEvent] ++ classOf[AnotherSensoryEvent]
EventsFlow anotherFlow = flow
    .remove(SomeSensoryEvent.class)
    .add(AnotherSensoryEvent.class);

It is also possible to combine classes, strings and other events flows:

    val unhappyFlow: EventsFlow = 
        happyFlow -- classOf[InteractionSucceeded] ++ "InteractionExhausted" +++ someErrorFlow
    EventsFlow unhappyFlow = happyFlow
        .remove(InteractionSucceeded.class)
        .add("InteractionExhausted")
        .add(someErrorFlow);

Events flows are compared ignoring the order of the events:

   "EventOne" :: "EventTwo" :: EmptyFlow == "EventTwo" :: "EventOne" :: EmptyFlow // true   
   EventsFlow.of("EventOne","EventTwo").equals(EventsFlow.of("EventTwo","EventOne")); // true   

While comparing events flows it does not matter if an event is provided as a class or as a string:

   classOf[EventOne] :: EmptyFlow == "EventOne" :: EmptyFlow // true   
   EventsFlow.of(EventOne.class).equals(EventsFlow.of("EventOne")); // true   

RecipeAssert

RecipeAssert is the starting point of all your assertions for the recipe instance.

To create a RecipeAssert instance a baker instance and a recipe instance id are required:

  val recipeAssert: RecipeAssert = RecipeAssert(baker, recipeInstanceId)
    RecipeAssert recipeAssert = RecipeAssert.of(baker, recipeInstanceId);

There is a simple way to assert if the events flow for this recipe instance is exactly the same as expected:

  val happyFlow: EventsFlow =
    classOf[SomeSensoryEvent] :: classOf[InteractionSucceeded] :: EmptyFlow
  RecipeAssert(baker, recipeInstanceId).assertEventsFlow(happyFlow)
    EventsFlow happyFlow = EventsFlow.of(
        SomeSensoryEvent.class,
        InteractionSucceeded.class
    );
    RecipeAssert.of(baker, recipeInstanceId).assertEventsFlow(happyFlow);

If the assertion fails a clear error message with the difference is provided:

Events are not equal:
    actual: OrderPlaced, ItemsReserved
  expected: OrderPlaced, ItemsNotReserved
difference: ++ ItemsNotReserved
            -- ItemsReserved

There are multiple methods to assert ingredient values.

RecipeAssert(baker, recipeInstanceId)
  .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value
  .assertIngredient("nullishIngredient").isNull // exists and has `null` value
  .assertIngredient("not-existing").isAbsent // ingredient is not a part of the recipe
  .assertIngredient("someListOfStrings").is(value => Assertions.assert(value.asList(classOf[String]).size == 2)) // custom
RecipeAssert.of(baker, recipeInstanceId)
  .assertIngredient("ingredientName").isEqual(expectedValue) // is equal to the expected value
  .assertIngredient("nullishIngredient").isNull() // exists and has `null` value
  .assertIngredient("not-existing").isAbsent() // ingredient is not a part of the recipe
  .assertIngredient("someListOfStrings").is(val => Assert.assertEquals(2, val.asList(String.class).size())); // custom

You can log some information from the baker recipe instance.

Note: But in most cases you probably should not have to do it because the current state is logged when any of the assertions fail.

RecipeAssert(baker, recipeInstanceId)
    .logIngredients() // logs ingredients
    .logEventNames() // logs event names
    .logVisualState() // logs visual state in dot language
    .logCurrentState() // logs all the information available
RecipeAssert.of(baker, recipeInstanceId)
    .logIngredients() // logs ingredients
    .logEventNames() // logs event names
    .logVisualState() // logs visual state in dot language
    .logCurrentState(); // logs all the information available

Quite a common task is to wait for a baker process to finish or specific event to fire. Therefore, the blocking method was implemented:

RecipeAssert(baker, recipeInstanceId).waitFor(happyFlow)
// on this line all the events within happyFlow have happened
// otherwise timeout occurs and an assertion error is thrown
RecipeAssert.of(baker, recipeInstanceId).waitFor(happyFlow);
// on this line all the events within happyFlow have happened
// otherwise timeout occurs and an assertion error is thrown

As you have probably already noticed RecipeAssert is chainable so the typical usage would probably be something like the following:

RecipeAssert(baker, recipeInstanceId)
    .waitFor(happyFlow)
    .assertEventsFlow(happyFlow)
    .assertIngredient("ingredientA").isEqual(ingredientValueA)
    .assertIngredient("ingredientB").isEqual(ingredientValueB)
RecipeAssert.of(baker, recipeInstanceId)
    .waitFor(happyFlow)
    .assertEventsFlow(happyFlow)
    .assertIngredient("ingredientA").isEqual(ingredientValueA)
    .assertIngredient("ingredientB").isEqual(ingredientValueB);