Elixir: A cleaner way to organize tests using ExUnit's named setup.
Tags: elixir, exunit, testing, todayilearned
Published: 2021-02-06 08:10:13

The dev team at [Vendorful](https://vendorful.com/) 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](https://dev-to-uploads.s3.amazonaws.com/i/cwahvioawvw55g84a6ru.jpg)

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.

```ruby
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
```

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](https://hexdocs.pm/ex_unit/master/ExUnit.Case.html#describe/2) 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:

```elixir
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
```

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:

```elixir
defp setup_cold_dish(%{size: size}) do
end
```

### How do I use it in the tests?

```elixir
describe "make COLD dishes from left over" do
  setup [:setup_cold_dish]
  it "", context do
    ...
  end
end
```

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

```elixir
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
```

### 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!
```elixir
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
```

### 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.