Dealing with fixtures in Scala

Dealing with fixtures in Scala

Sometimes you come across something that you really want to use it but you are not very convinced if you are using it the right way; this exactly is what happened to me with fixtures.

According to ScalaTest:

"a test fixture is composed of the objects and other artifacts, like files, sockets, database connections, etc, which tests use to do their work."

At first glance, I thought: "Well, this could be the next code shortener to my projects". And I was quite right, but I had to take care of certain things, specially case classes with mutable objects.

Fixtures and mutable objects

I was trying to test some cascading methods from a case class, kind of a fluent interface. Let's imagine we have our class pizza:

case class Pizza(flavour: String, protected var ingredients: List[String]) {

  private def removeIngredients(ingredientsToRemove: List[String]): this.type = {
    ingredients = ingredients diff ingredientsToRemove
    this
  }

  def isAllergicToSomething(allergens: List[String]): Unit =
    if (allergens.exists(ingredients.contains)) removeIngredients(allergens)
}
Our amazing case class Pizza with some arguments

What we have here is a Pizza which can change its ingredients depending of the allergens of who is going to eat it. By using the public method isAllergicToSomething() we can subtract all the ingredients that match any allergens, thanks to our private def removeIngredients().

But, how do we test that, when isAllergicToSomething() is called, the dangerous ingredients are really gone from the list of ingredients ? Are fixtures suitable for this?

As a lazy developer, I thought of fixtures and how I can use them to save some time and effort, so I started to create a fixture, according to ScalaTest:

trait AllergicCase {
  val allergens: List[String] = List("Tomato")
  val pizzaMargherita: Pizza = Generators.pizzaMargherita
}
My very first fixture. Heartwarming.

What I did is create a Trait in which I define two variables: allergens and an instance of a Pizza, which is stored in another file, Generators.scala:

object Generators {
  val flavour: String = "Margherita"
  val ingredients : List[String] =
    List("Tomato", "Mozzarella", "Basil", "Salt", "Olive Oil")
  val pizzaMargherita: Pizza = Pizza(flavour, ingredients)

}
Generators.scala

Doing some tests

So I decided to write some tests. I like to use FlatSpec because it is more verbose:

import org.scalatest.flatspec.AnyFlatSpec

class PizzaTest extends AnyFlatSpec {

  trait AllergicCase {
    val allergens: List[String] = List("Tomato")
    val pizzaMargherita: Pizza = Generators.pizzaMargherita
  }

  behavior of "Pizza"

  it should "not have Tomato in ingredients list if diner is allergic to Tomato" in new AllergicCase {
    val myPizza: Pizza = pizzaMargherita
    myPizza.isAllergicToSomething(allergens)

    assert(!myPizza.ingredients.contains("Tomato"))
  }
 
 }
PizzaTest.scala

I ran the test and it was OK. Nice, but what if I would like to test with other allergen? Also, this time we are going to test if Tomato remains:

  it should "have Tomato but not Basil in ingredients list if diner is allergic to Basil" in new AllergicCase {
    val myPizza: Pizza = pizzaMargherita
    myPizza.isAllergicToSomething(List("Basil"))

    assert(myPizza.ingredients.contains("Tomato"), "it should contain tomato")
    assert(!myPizza.ingredients.contains("Basil"), "it should not contain Basil")
  }
The second test, this time with Basil.

Let's run both tests:

List("Mozzarella", "Salt", "Olive Oil") did not contain "Tomato" it should contain tomato
Oops, test failed.

What I see here is that, for some reason, it remains the instance from the first test to the another. And the answer may be easy for you, that are reading right now this and I guided you to this error. But trust me, I spent a little bit more of time figuring out what was going on. Not an ideal situation for a lazy dev.

The problem is  Generators.scala; while I was using the trait AllergicCase, I was storing an instance of Pizza created on Generators.scala and not generating it within the trait, so we were actually modifying the instance on Generators object and not creating one for each test.

The solution

So I decided to make some changes. Let's get rid of Generators.scala and instantiate all of it within the trait:

trait AllergicCase {
  val flavour: String = "Margherita"
  val ingredients : List[String] =
    List("Tomato", "Mozzarella", "Basil", "Salt", "Olive Oil")
  val allergens: List[String] = List("Tomato")
  val pizzaMargherita: Pizza = Pizza(flavour, ingredients)
}
Proper trait AllergicCase. This is what I'm talking about.

Now I rerun the tests, and both were OK. Sweet!

Conclusion

Fixtures are a great tool for lazy devs, it encapsulates the environment by creating within the test itself all the variables and objects you need to run the test, and it does every time you run a specific test, so it gives you freedom to start over again to test everything from one trait. While this is great, it may lead you to create one specific case valid for all of your tests. Remember (note to self) that you can create more than just one trait to create all the environments/cases you need to assure your code is bombproof.

If you want to discuss about this, feel free to tweet me.