jyotigautam

jyotigautam

Testing an elixir function using Mock

I have a Product schema which has a UPI(unique product identifier) eg. A985748BNG6784C . This is an autogenerated unique product identifier.

I have a function upi_generate() which calls another external function gen_nano_id() to generate this random unique id.

If by chance, the id generated by gen_nano_id() has already been generated, the function upi_generate() calls itself recursively till the time gen_nano_id() generates a unique id. Thus generating a unique UPI .

gen_nano_id() can sometimes return duplicate ids and for that purpose I have written the below code with recursive call.

  def gen_nano_id() do  #external function
      Nanoid.generate(10, "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ")
  end

  # TODO: write test case for this
  def upi_generate() do # required function
      upi = "A" <> gen_nano_id() <> "C"

      case get_product_with_upi(upi) do
         nil ->
            upi 
         _ ->
            upi_generate()
      end
  end

  # Check if product with upi already exists
  defp get_product_with_upi(upi) do
      from(p in "snitch_products", select: p.upi, where: p.upi == ^upi) 
      |> Repo.one()
  end

Now, I have to test the id regeneration logic for duplicate ids.

My testing approach involves following logic. Create two products with duplicate UPI and try to reach the _ part of the case comparison.

For this purpose, I have mocked(I don’t control the behaviour of this function) the gen_nano_id() .

Now, the problem that I am facing is mocking results in creation of always the same ids no matter what and I go in an infinite loop.

I am not able to figure out a way to reach the exit condition(nil) part of case comparison with this mocking approach of gen_nano_id .

Marked As Solved

dimitarvp

dimitarvp

First, here’s a stateful Agent that holds a counter and a simple logic that forces it to return the same value several times, before starting to return random values afterwards. Like this:

defmodule NanoidMock do
  @moduledoc ~S"""
  Mocks Nanoid value producer with a simple counter.
  Initialize with `start_link(0)` to reproduce the following:
  For the first 4 invocations of `get_nano_id` the counter
  will move from 0 to 3 and will always return 0.
  After 4 invocations of `get_nano_id` (when the counter
  reaches 4) and onwards, it will return a random number.
  """

  use Agent

  # Initialize with a non-negative integer counter.
  def start_link(val) when val >= 0 do
    Agent.start_link(fn -> val end, name: __MODULE__)
  end

  def stop(), do: Agent.stop(__MODULE__)

  def clear(), do: Agent.update(__MODULE__, fn(_) -> 0 end)

  def get_nano_id() do
    new_val = Agent.get_and_update(__MODULE__, fn(x) -> {x, x + 1} end)
    case div(new_val, 4) do
      0 -> 0
      _ -> :rand.uniform(1_000_000)
    end
  end
end

Then let’s compose a test class:

defmodule NanoidTestWithMock do
  def get_nano_id(), do: NanoidMock.get_nano_id()

  def upi_generate() do
    upi = "A#{get_nano_id()}C"

    case get_product_with_upi(upi) do
      nil -> upi
      _   -> upi_generate()
    end
  end

  defp get_product_with_upi(upi) do
    case upi do
      "A0C" ->
        IO.puts "Duplicated UPI=A0C. Retrying."
        %{error: "product already exists"}

      _ ->
        nil
    end
  end

  def test() do
    NanoidMock.start_link(0)
    upi = upi_generate()
    NanoidMock.stop()
    IO.puts "Successfuly got unique UPI=#{upi}"
  end
end

Put these modules in a single Elixir file and then run NanoidTestWithMock.test() in an iex session.

(Or add the above expression at the bottom of the file and just tell Elixir to run it.)

You should see something like this:

Duplicated UPI=A0C. Retrying.
Duplicated UPI=A0C. Retrying.
Duplicated UPI=A0C. Retrying.
Duplicated UPI=A0C. Retrying.
Successfuly got unique UPI=A394747C

Hope that helps.

EDIT: As for mocking the Nanoid library itself, I’ll leave that exercise to you.

Also Liked

gausby

gausby

Don’t mock it; instead pass in the random uuid as an argument; have it return something like {:error, :uuid_already_in_use} in the function generating the upi so you can try with a new one if the UUID should already be taken.

In general I think it is a good idea to pass this kind of data into functions, including timestamps, as it will make testing a lot easier.

jyotigautam

jyotigautam

Thanks for suggesting the approach. This nicely tests the scenario of duplicate UPIs.

Last Post!

jyotigautam

jyotigautam

Thanks for suggesting the approach. This nicely tests the scenario of duplicate UPIs.

Where Next?

Popular in Questions Top

New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New

Other popular topics Top

vonH
When I run the Plug and I recompile I wind up having to use Ctrl C to quit iex and start again. Witht the help of rlwrap I can use the cu...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54996 245
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 49134 226
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement