The Senior Way of Testing in Elixir Part 1

Pau RiosaPau Riosa
5 min read

Five years ago, writing unit tests in Elixir was both exciting and straightforward to me, and it remains so even today. However, just last week, I discovered something new that enhances this experience.

Taking advantage of Tags and Setups

Scenario #1

Let's discuss how I used to write my tests before making this discovery. Suppose I have a series of unit tests to identify different users.

defmodule MyApp do
    setup [:register_and_log_in_user]

    describe "render login user" do
        test "as member"
        test "as admin"
        test "as manager"
    end

    defp register_and_log_in_user(context) do
       # rest of the code
    end
end

To describe them individually based on their roles or other criteria, I organized the code by grouping them into their own describe blocks. Then, I created distinct setup functions to support each group.

This setup is beneficial for me because the tests are independent of each other, each having its own setup. Additionally, I can create more setup functions as needed.

defmodule MyApp do

    describe "member" do
        setup [:register_and_log_in_user_as_member]
        test "render login user as member"
    end

    describe "manager" do
        setup [:register_and_log_in_user_as_manager]
        test "render login user as manager"
    end

    describe "admin" do
        setup [:register_and_log_in_user_as_admin]
        test "render login user as admin"
    end

    defp register_and_log_in_user_as_member(context) do
       # rest of the code
    end

    defp register_and_log_in_user_as_manager(context) do
       # rest of the code
    end

    defp register_and_log_in_user_as_admin(context) do
       # rest of the code
    end
end

But on the other hand, I found it hard to read.

Imagine creating more setup blocks for each of them. While this approach can work, it may lead to redundancy. Second, at a glance, I found it difficult to understand what is happening in each test.

But what if we could improve this a little bit more using tags.

Discovery #1: The Power of Tags

Let's return to using the old setup for the test. We can simply use tags in our unit tests, like this.

First, we keep the setup block to handle the registration and login process for each test.

defmodule MyApp do
    setup [:register_and_log_in_user]
    # rest of the code
end

Second, we use tags to specify which type of user is logging in for each unit tests.

defmodule MyApp do
    # rest of the code

    describe "render login user" do
        @tag user_type: :member
        test "as member"

        @tag user_type: :admin
        test "as admin"

        @tag user_type: :manager
        test "as manager"
    end

    # rest of the code
end

Third, we modify the function that registers and logs in the user by using their tag value from the context map.

defmodule MyApp do
    # rest of the code

    defp register_and_log_in_user(context) do
        user = 
            case context[:user_type] do
               :member -> # rest of the code
               :admin -> # rest of the code
               :manager -> # rest of the code
            end

        {:ok, user: user}
    end
end

This is how the code looks like.

defmodule MyApp do
    # rest of the code
    setup [:register_and_log_in_user]

    describe "render login user" do
        @tag user_type: :member
        test "as member"

        @tag user_type: :admin
        test "as admin"

        @tag user_type: :manager
        test "as manager"
    end

    defp register_and_log_in_user(context) do
        user = 
            case context[:user_type] do
               :member -> # rest of the code
               :admin -> # rest of the code
               :manager -> # rest of the code
                _default -> # maybe this is for guest?
            end

        {:ok, user: user}
    end
end

What improvements have been made here?

  1. We have only one setup block that handles both the registration and login processes. We use a case statement to determine which user we are dealing with.

  2. We have only one describe block to explain what functionality we are testing.

  3. By using tags, we can easily identify which user we are using.

Amazing right? But here’s the challenge…

What if we want to describe the features available to a logged-in user who is registered as a daily, weekly, or monthly subscriber? How are we going to do that?

Discovery #2: Consistent Setup

By taking advantage of setup blocks, we can describe and test the functionality available to a user based on their type of subscription.

defmodule MyApp do
    # rest of the code
    setup [:register_and_log_in_user, :setup_subscription_type]

    describe "with daily subscription type" do
        @tag [user_type: :member, subscription_type: :daily]
        test "as member, I could only see this..."

        @tag [user_type: :admin, subscription_type: :daily]
        test "as admin, I could see that..."

        @tag [user_type: :manager, subcription_type: :daily]
        test "as manager, I cannot..."
    end

    describe "with weekly subscription type"
    describe "with monthly subscription type"

    defp setup_subscription_type(%{subscription_type: :daily} = _context) do
        # rest of code
    end

    defp setup_subscription_type(%{subscription_type: :weekly} = _context) do
        # rest of code
    end

    defp setup_subscription_type(%{subscription_type: :monthly} = _context) do
        # rest of code
    end

    defp setup_subscription_type(_context) do
        # rest of code
    end

    # rest of the code

    # rest of the code
end

What additional advantages do we have here?

  1. We still use one setup block.

  2. We added extra tags to describe the type of subscription the current registered and logged-in user has.

  3. We use pattern matching to handle different functions for each subscription type.

Overall

This is how my unit test looks like now.

defmodule MyApp do
    # rest of the code
    setup [:register_and_log_in_user, :setup_subscription_type]

    describe "render login user" do
        @tag user_type: :member
        test "as member"

        @tag user_type: :admin
        test "as admin"

        @tag user_type: :manager
        test "as manager"
    end

    describe "with daily subscription type" do
        @tag [user_type: :member, subscription_type: :daily]
        test "as member, I could only see this..."

        @tag [user_type: :admin, subscription_type: :daily]
        test "as admin, I could see that..."

        @tag [user_type: :manager, subcription_type: :daily]
        test "as manager, I cannot..."
    end

    describe "with weekly subscription type"
    describe "with monthly subscription type"

    # we can improve this by using pattern matching based from their user_type
    defp register_and_log_in_user(context) do
        user = 
            case context[:user_type] do
               :member -> # rest of the code
               :admin -> # rest of the code
               :manager -> # rest of the code
                _default -> # maybe this is for guest?
            end

        {:ok, user: user}
    end

    defp setup_subscription_type(%{subscription_type: :daily} = _context) do
        # rest of code
    end

    defp setup_subscription_type(%{subscription_type: :weekly} = _context) do
        # rest of code
    end

    defp setup_subscription_type(%{subscription_type: :monthly} = _context) do
        # rest of code
    end

    defp setup_subscription_type(_context) do
        # rest of code
    end

    # rest of the code
end

I understand there is room for improvement here. I'm not suggesting you use this immediately, but I hope you find it helpful when writing your future tests.

Stay tuned for Part 2.

Happy coding.

0
Subscribe to my newsletter

Read articles from Pau Riosa directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pau Riosa
Pau Riosa

🎥 Youtuber 💻. Software Developer 📕 Blogger I am software developer who has passion for career development and software engineering. I love building stuff that can help people all around the world. Let's connect and share that idea!