DEV Community

Cover image for Speed-up Building your Elixir Unit Tests with Fictitious
Nyoman Abiwinanda
Nyoman Abiwinanda

Posted on

Speed-up Building your Elixir Unit Tests with Fictitious

Imagine the following situation, you have several APIs to build or complete in a day or days. You started to work on them one by one, finish each of them in about an hour or two, feels satisfied when you finish one and that satisfaction makes you feel ready to move on to another tasks but only to realise at the end that you forgot to make the unit tests and making them could cost you about the same time or even more time than you need to finish up your APIs. Have you ever been in this kind of situation? Well... I do, sometimes it bother me so much that I decided to skip the unit test only to realised in the long run that this degrade my productivity because things just breaks unnoticedly as your applications are becoming bigger, complex, and more people are working on the same code base.

Why Unit Test can be Tedious?

Before we even touch our keyboard and just think about unit test briefly in our head, unit testing a code seems to be a simple task especially in a functional world. Typically these are the steps when we test our code:

Alt Text

  1. Prepare your input to be fed to your function or system under test.
  2. Call your function or invoke what ever things that you trying to test with the test input that you generate.
  3. Check whether your function produce the expected output.

Basically these steps can be summarised with a phrase Prepare, Test, Assert. Sounds pretty simple right? Sure, but, this is usually what you think about it before you touch your keyboard and start making your real test though.

Sometimes the step that eats up a lot of your time isn't really the testing part but preparing the test or mock data for your system under test. Consider the following application. You have a function, lets called it like_a_comment() that increment the number of like for a blog post's comment by 1

Alt Text

The testing part is rather simple, you pass a sample comment to the function like_a_comment() and then checked at the end whether the number of likes for that comment is incremented by one. However we might not realised that in obtaining the input comment it self, it might require us to walk through several steps. For example, before we have a comment we need to:

  • Have a post that the comment is belonged to.
  • Even before that a user must exist as the owner of that post.

So you realised that the scope of your code is kind of more illustrated by the following diagram

Alt Text

You might be saying Hey, can we just wrapped the steps to create a comment in a function and reuse it through out other tests? Sure we can, in fact that is exactly what I had been doing previously. However, it only cover that one specific case. As my application gets bigger, more complex, and new data are introduced into the system, more data (utility) functions that I need to prepare before I could start writing the actual test. Would it be nice if there is an out of the box tool where I could just say I want a comment with no like to be created and I don't care how the comment is supposed to created? Turns out there is one in elixir.

Make your Unit Test Data with Fictitious

To address the issue that we discussed previously, I created a library called Fictitious which I open source to hex.pm for anyone to use. Fictitious, like its name, is a tool that enables you to create a fictitious data in elixir. It helps you to create mock data for your unit test without having the hassle of preparing the data in an convoluted order according to their associations that they have. Fictitious will ensure that whatever schema that you give as an input will create the related data for you.

Consider the following two schemas Person and Country

Person Schema
defmodule YourApp.Schema.Person do
  use Ecto.Schema
  import Ecto.Changeset
  alias YourApp.Schema.Country

  schema "persons" do
    field :name, :string
    field :age, :integer
    field :gender, :string
    field :email, :string
    belongs_to :nationality, Country, references: :id, foreign_key: :country_id, type: :id

    timestamps()
  end
end
Enter fullscreen mode Exit fullscreen mode
Country Schema
defmodule YourApp.Schema.Country do
  use Ecto.Schema
  import Ecto.Changeset
  alias YourApp.Schema.Person

  schema "countries" do
    field :name, :string
    has_many :people, Person, foreign_key: :country_id

    timestamps()
  end
end
Enter fullscreen mode Exit fullscreen mode

where Person is belonged to a Country. Let say in this case a person must belonged to a country. This means that country_id can't be null and when there is a value for it, it must references to an existing id of a country. This kind of data schemes could make data preparation in a test becomes tedious however this is one problem where fictitious could help you.

For example, calling fictionize/1 to Country will makes a fictitious country:

iex> Fictitious.fictionize(YourApp.Schema.Country)
{:ok, %YourApp.Schema.Country{
  __meta__: #Ecto.Schema.Metadata<:loaded, "countries">,
  id: 67,
  name: "B8LemwxB8ULP4NLUaFnKfwWkMmBYy8BTytkSN2PiL1UTO47yRM",
  people: #Ecto.Association.NotLoaded<association :people is not loaded>,
  inserted_at: ~U[2020-04-31 06:19:27Z],
  updated_at: ~U[2020-04-31 06:19:27Z]
}}
Enter fullscreen mode Exit fullscreen mode

however calling fictionize/1 to Person will creates a person by creating the country as well:

iex> {:ok, person} = Fictitious.fictionize(YourApp.Schema.Person)
{:ok, %YourApp.Schema.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "persons">,
  id: 725,
  name: "bElHKj9zVwnkLRpO4Y23yon9n80gm1yeAEL4PgtgkxBc0p2Y7C",
  age: 364,
  gender: "dF1O5Eq4ombjzah",
  email: "hpOXdOriGA9xaMhnwese40PqqL2Ine",
  nationality: #Ecto.Association.NotLoaded<association :nationality is not loaded>,
  nationality_id: 401,
  inserted_at: ~U[2020-04-31 06:19:27Z],
  updated_at: ~U[2020-04-31 06:19:27Z]
}}

iex> YourApp.Repo.preload(person, :nationality)
%YourApp.Schema.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "persons">,
  id: 725,
  name: "bElHKj9zVwnkLRpO4Y23yon9n80gm1yeAEL4PgtgkxBc0p2Y7C",
  age: 364,
  gender: "dF1O5Eq4ombjzah",
  email: "hpOXdOriGA9xaMhnwese40PqqL2Ine",
  nationality: %YourApp.Schema.Country{
    __meta__: #Ecto.Schema.Metadata<:loaded, "countries">,
    id: 401,
    name: "lcb1e86TY6RSccL6vPGjXOv43gnp1t",
    people: #Ecto.Association.NotLoaded<association :people is not loaded>
    inserted_at: ~U[2020-04-31 06:19:27Z],
    updated_at: ~U[2020-04-31 06:19:27Z]
  },
  nationality_id: 401,
  inserted_at: ~U[2020-04-31 06:19:27Z],
  updated_at: ~U[2020-04-31 06:19:27Z]
}
Enter fullscreen mode Exit fullscreen mode

In this case, having the country to be created automatically removes the trouble of having to prepare country data before a person could be created or in other word Fictitious ensures that you get the targeted or wanted entity to be created.

You might be thinking, I don't want the data to be completely random, I want to override the data with some values. How can I do that? No problem, fictitious allows you to override any field in the schema by simply providing the value at the second argument.

iex> {:ok, country} = Fictitious.fictionize(YourApp.Schema.Country, name: "Indonesia")
{:ok, %YourApp.Schema.Country{
  __meta__: #Ecto.Schema.Metadata<:loaded, "countries">,
  id: 7914,
  name: "Indonesia",
  people: #Ecto.Association.NotLoaded<association :people is not loaded>,
  inserted_at: ~U[2020-04-31 06:19:27Z],
  updated_at: ~U[2020-04-31 06:19:27Z]
}}
Enter fullscreen mode Exit fullscreen mode

it could even override the related entity by passing it directly as follows.

{:ok, person} = Fictitious.fictionize(YourApp.Schema.Person, nationality: country)
{:ok, %YourApp.Schema.Person{
  __meta__: #Ecto.Schema.Metadata<:loaded, "persons">,
  id: 451,
  name: "ZFvtidsGOPh6OymYJk529bL2QT9KMZic2A0ietddl2RWy",
  age: 150940,
  gender: "rHZYpbDgJQokDX2vSpSfWUmELrTb9f",
  email: "xmcuHrJvotjAQz6itQnZtoMp",
  nationality: #Ecto.Association.NotLoaded<association :nationality is not loaded>,
  inserted_at: ~U[2020-04-31 06:19:27Z],
  updated_at: ~U[2020-04-31 06:19:27Z]
}}
Enter fullscreen mode Exit fullscreen mode

More About Fictitious

Now that you know what fictitious is capable of, I hope it could help you just like it helps me in making unit test faster in elixir and becoming more productive. It definitely one of my additional or alternative tools to help me in creating unit test in elixir and hopefully it can be added to yours as well. If you are interested to know more about fictitious feel free to read the official documentation here or if you turns out to have an idea to improve it and interested to make it better feel free to contribute in the following GitHub repository.

Happy unit testing :)

Discussion (1)

Collapse
allanmacgregor profile image
Allan MacGregor 🇨🇦

Thanks for sharing can you clarify how does Fictitious differ from ExMachina ?