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