DEV Community

mreigen
mreigen

Posted on • Updated on

Elixir: A cleaner way to organize tests using ExUnit's named setup.

The dev team at Vendorful take code quality seriously. We invest in writing tests for every bug fix and new feature being implemented and in well reviewed merge requests. As any good developer should know from their past experience that with good test coverage they can actually move faster as a team in the long run. However, in addition to having good test coverage, writing human-readable code and tests is equally important. Personally, my feeling of having to work with a neatly nested and organized test suite set up but extremely difficult to follow is almost comparable to having to walk through a wild maze to find something that is not very worthwhile, every time.

Alt Text

Coming from Ruby on Rails background, I used to use RSpec to write my tests. While coding with Elixir and using ExUnit to write tests nowadays, I caught myself finding ways to nest RSpec context blocks inside each other. Let's find a simple example: you wanted to build a chef robot to prepare you dinners base on the left over food in your fridge. Depending on what's available in your kitchen, it'll choose how to prepare the appropriate dishes.

Nested contexts in RSpec

This is how I would write TDD tests for this scenario in RSpec.

describe "make_dish_from_left_over" do
  context "with dishes that are served cold" do
    # before: set up cold dishes
    # ...
    # end
    context "seasoning available" do
      # before: set up seasoning
      # ...
      # end
      expect(add_seasoning).to be_truthy
    end
    context "extra protein available" do
      # before: set up extra protein
      # ...
      # end
      expect(add_protein).to be_truthy
    end
  end

  context "with dishes that are served hot" do
      # before: set up hot dishes
      # ...
      # end
    context "microwave available" do
      # before: set up microwave
      # ...
      # end
      expect(warm_up_by_microwave).to be_truthy
    end
    context "microwave NOT available" do
      # before: set up stove top
      # ...
      # end
      expect(warm_up_by_stove_top).to be_truthy
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

As you can imagine, if these tests are all filled up and more nested contexts are added, such as: microwave settings depending on size of dishes, stove top heat amount and time, etc... it will become too long and too complicated to read and follow, especially for other devs who haven't worked on this area of the robot. Also, there is no way to write nested "contexts" like this in Elixir's ExUnit - and there is a good reason for that.

By forbidding hierarchies in favor of named setups, it is straightforward for the developer to glance at each describe block and know exactly the setup steps involved. - ExUnit documentation

With ExUnit, what you can, and want to do is to use named setups.

What are named setups?

I have to admit that the documentation for named setup in ExUnit is kind of buried somewhere in the middle of ExUnit.Case documentations. Named setups are simply configurations of tests used to put together test data for the following tests. Each setup is actually just a function that configures the test data and returns the variables to be used in the tests themselves. If you are familiar with writing tests in ExUnit then these configuration functions are exactly the setup blocks. For example:

defp setup_cold_dishes(context) do
  temp = 35
  size = if context.size, do: size, else: :small
  dish = set_temperature(temp)
  |> set_size(size)
  |> set_time_in_fridge(:15_hours)
  |> build_dish

  %{temp: temp, size: size, cold_dish: dish}
end
Enter fullscreen mode Exit fullscreen mode

Note that a context variable is passed in this setup function so that the previous configurations can be accessed in this setup as context.size in this case. You can also grab the size variable out of this context right in the function signature like so:

defp setup_cold_dish(%{size: size}) do
end
Enter fullscreen mode Exit fullscreen mode

How do I use it in the tests?

describe "make COLD dishes from left over" do
  setup [:setup_cold_dish]
  it "", context do
    ...
  end
end
Enter fullscreen mode Exit fullscreen mode

Let's make it even nicer by combining the condition "seasoning available":

describe "make COLD dishes from left over when seasoning available" do
  setup [:setup_cold_dish, :set_seasoning]
  it "", context do
    assert add_seasoning
  end
end
Enter fullscreen mode Exit fullscreen mode

Passing variables to other setup functions

Note that if you have multiple setup functions in sequence like this, the later ones on the right can read the "context" variables returned by the previous ones on the left. So :set_seasoning can actually access temp, size and cold_dish variables from :setup_cold_dish but we just not doing anything with them in this scenario.

Putting the pieces together

Now let's write the whole thing with combined conditions!

describe "make COLD dishes when seasoning available" do
  setup [:setup_cold_dish, :set_seasoning]
  it "", context do
    assert add_seasoning
  end
end

describe "make COLD dishes when extra protein available" do
  setup [:setup_cold_dish, :set_protein]
  it "", context do
    assert add_protein
  end
end

describe "make HOT dishes when microwave available" do
  setup [:setup_hot_dish, :set_microwave]
  it "", context do
    assert use_microwave
  end
end

describe "make HOT dishes when stove top available" do
  setup [:setup_hot_dish, :set_stove_top]
  it "", context do
    assert use_stove_top
  end
end
Enter fullscreen mode Exit fullscreen mode

Conclusion

The tests are now flattened, much cleaner and easier for other developers (and for me at a later time) to read and follow at a glance. And the beauty of it is: no more nested context/describe blocks, no more mazes but with shared setup functions and cleaner code!

Although the above example scenario is kept very simple to demonstrate the basic functionalities of named setups, but I hope you could get the idea and apply it to your test suite when you need it.

Top comments (0)