How outside-in TDD can protect your domain

How outside-in TDD can protect your domain
Down Arrow

Looking at a blank canvas and wondering where to put the first brush stroke is anxiety inducing. A similar feeling exists when developers have just been handed a user story and are wondering where to write the first test to start driving out user value.

There are two major schools of thought on this subject, outside-in and inside-out.

 

In this post, I’d like to focus just on how these techniques relate to the domain language of an application. At its core, domain-driven design (DDD) seeks to have the software structured in a way that reflects the core business concepts.

 

Let’s use this totally useful and real user story as a means by which to see how these testing strategies help reflect the business domain (or not).

 

As a User

When I go to my profile

Then I see a list of my three favorite ice cream flavors

 

So where to write the first test? Let’s say that we already are familiar with the `user` api, and know where we can get that info. So, we could start working at the repository level and work our way toward the front end. This is called inside-out testing. 

 

Alternatively, we could write a feature test (a test that imitates the behavior of the user) that would fail until we had implemented the whole feature. This feature test would first fail because there aren’t any ice cream flavors on any of the data models, thus guiding us deeper into the app until we’ve finished the feature. As you’ve probably already guessed, this is called outside-in testing.

 

There are a bunch of great posts about the trade-offs and differences between inside-out and outside-in, and so let’s dive right into the nitty gritty. 

 

Inside-out TDD

 

Inside-out TDD 

 

All right! We’ve seen the user story and start out by looking at the database. After trying to remember our SQL commands (I always forget them) we see that there are columns in the `users` table labeled “favorite_flavor1”, “favorite_flavor2”, and “favorite_flavor3.” This story seems pretty straightforward! 

 

Since we have this data right here and are testing inside-out, we write all the tests to extend the `user` model to include these fields. Then we move up the stack until the flavors are displayed on the page. Everything looks good, the product team signs off, and so we ship the code. Our `user` model (all the way from the repository to the front end) looks something like this:

 

currentUser: {

  id: 53,

  name: “name”,

  …

  favorite_flavor1:  “Chocolate”,

  favorite_flavor1:  “Pistachio”,

  favorite_flavor1:  “Mint Chocolate Chip”,

  ...

}

The feature is a hit! 🎉

 

The feature is a hit!

 

Who doesn’t like ice cream? The product team learns that because ice cream is so popular, we need to provide more information about the flavors—let’s say, the brand.

 

So again we head toward the innermost part of our app: the database. The relevant ice cream brand data is stored in an `ice_cream` table in our database, and it looks something like this:

 

{

  id: 192,

  flavor: “Chocolate”,

  brand: “Auntie Melissa’s Totally Real Brand”,

  address1: “123 Somewhere Lane”

  ...

}

 

So what do we do? It seems like we could just use the `favorite_flavor1` string from the user model and match that on the flavor field in the `ice_cream` table. But wait! Lots of brands have a “Chocolate” ice cream. We have no way of knowing _which_ chocolate the `favorite_flavor1` field is referring to. Luckily, there exists a table that maps favorite ice cream flavors to users (something like `id || user_id || ice_cream_id`).

 

Now that we know where to get the information, we need to get it from the repository up to the front end. Since this story requires more information about each user’s favorite ice creams, it makes sense to extend the `user` model similar to what we did in the last story. It seems like the easiest way to do this would be to remove the original three columns from the `users` object and add the `ice_cream` models to the `user` model. It would end up looking like this:

 

currentUser: {

  id: 53,

  name: “name”,

  …

  favorite_flavor1:  “Chocolate”,

  favorite_flavor1:  “Pistachio”,

  favorite_flavor1:  “Mint Chocolate Chip”,

  favoriteIceCreams: [

    {

      id: 192,

      flavor: “Chocolate,

      brand: “Auntie Melissa’s Totally Real Brand”,

      address1: “123 Somewhere Lane”

      ...

    },

    {

      id: 192,

      flavour: “Pistachio,

      brand: “Jar Jar’s Blue Milk Creamery”,

      address1: “123 Somewhere Lane”

      ...

    },

    ...

  ]

  ...

}

 

Even with all the ellipses hiding the rest of the code, and ignoring that edge case, we see that this is getting out of hand!

 

This is getting out of hand, now there are two of them

 

(Let’s just assume the “two” that Nute Gunray is referring to are domain concepts.)

 

Okay, so let’s say you agree that that is too much and that we can just expand the `user` model with the brand information, like so: 

 

currentUser: {

  id: 53,

  name: “name”,

  …

  favorite_flavor1:  “Chocolate”,

  favorite_flavor1_brand: “Auntie Melissa’s Totally Real Brand”,

  favorite_flavor1:  “Pistachio”,

  favorite_flavor2_brand: “Jar Jar’s Blue Milk Creamery”,

  favorite_flavor3:  “Mint Chocolate Chip”,

  favorite_flavor3_brand: “Freezination”

  ...

}

 

Both of these implementations successfully meet all the acceptance criteria and are totally shippable, but our `user` object has become clunky, contains information that’s not specific to the user, and is hard to reason about. In domain-driven design, we should try to keep our domain concepts separate, even if there are connections between them. 

 

Outside-in TDD

 

Outside-in testing is a way to help keep these domain concepts clean, even from the start. Let’s pull a Coldplay (go back to the start) and take a look at the original model.

 

Outside-in TDD

 

Starting with a mentally clean slate from the front end, how do we want the data to look? I would imagine an array of strings that tells me what the three favorite ice cream flavors are. Something like: 

 

currentUser: {

   …

  favoriteIceCreamFlavors: [

    “Chocolate”,

    “Pistachio”,

    “Mint Chocolate Chip”

  ]

  ...

}

 

Looks simple, and similar enough to our database structure to be easily implemented. The only difference is that our service layer takes the values in those three fields (`favorite_flavor1`, `favorite_flavor2`, and `favorite_flavor3`) and puts them into an array that we would pass upwards, instead of leaving them as is on the `user`. 

 

Things get more interesting when we take on the second user story, to include the brand. Since we start at the front end we have to look at that model first, and the existing field `favoriteIceCreamFlavors`. It might make sense to change the field name to `favoriteIceCreams` and to make the contents be objects that each have a flavor and a brand name: 

 

currentUser: {

  …

  favoriteIceCreams: [

      {name: “Chocolate”, brand: “Auntie Melissa’s Totally Real Brand”},

      {name: “Pistachio”, brand: “Jar Jar’s Blue Milk Creamery”},

      {name: “Mint Chocolate Chip”, brand: “Freezination”}

  ]

  ...

}

 

But this already has a bit of code smell. Why are we changing our `user` object to take in another object’s data? What if we took advantage of this expansion of ice cream knowledge to think of ice cream as its own domain concept, and pulled it out of the front-end user model?

 

currentUser: {

  id: 53,

  name: “name”,

  ...

},

favoriteIceCreams: [

  {name: “Chocolate”, brand: “Auntie Melissa’s Totally Real Brand”},

  {name: “Pistachio”, brand: “Jar Jar’s Blue Milk Creamery”},

  {name: “Mint Chocolate Chip”, brand: “Freezination”}

]

 

so fresh and so clean

 

Wow! Now that looks pretty to the point. All the logic of figuring out which ice creams to show happens back in the service layer, and the front-end is free of extraneous data. The complexity we add to the service layer we make up for with a clean, organized data model.

 

Wrapping up

 

Both inside-out and outside-in TDD are valid strategies that can be used to test drive and ship great code. Due to their different starting points, they tend to push complexity along their paths. Inside-out TDDing can be simpler in that the front end just gets to grab the data it needs from whatever objects are there. This tight coupling of the front-end models to the database does come with the complexity of being harder to reason about and difficult to refactor.

 

Through this example, we’ve seen how outside-in TDD’s focus on the user experience can help us shape our domain models. Starting with the end state allows us to break up code along domain boundaries and reflect how models will be used and even expanded upon.

 

I hope this helps you have a clearer entry point to your next user story. Happy testing!

Aaron Foster Breilyn (afb)

Aaron Foster Breilyn (afb)

Principal Software Engineer

No items found.
green diamond