I am totally new to Elixir, but when using Node we do a lot of early returns, but I'm having some problems to make an early-return-like code work in my integration tests. This is my problem:
I have this method in my controller that receives a JSON as param, I need to validate if the JSON contains some properties (key and type), if it doesn't I want to send an error to the client without the need to execute the rest of the method. This works perfectly when I make a request using Insomnia or Postman, but it doesn't work in my integration tests. The code below the conditional executes even when the conditional is falsy.
This is my method:
def create(conn, body) do
if !PayeeContext.check_for_required_fields(body), do: conn
|> send_resp(400, "Please provide Key and Type parameters")
%{"key" => key} = body
%{"type" => type} = body
validation_func = PayeeContext.validation_map(type)
case validation_func.(key) do
{:ok, _} ->
conn |> send_resp(200, "Request created successfully")
{:error, message} -> conn |> send_resp(400, message)
end
end
and this is my test
test "returns 400 if Key prop is missing", %{conn: conn} do
params = %{
key: "44187221816",
account: %{
account_type: "checking_account",
account_number: "00028363-6",
branch: "0001"
},
owner: %{
name: "Eve Montalvão"
}
}
response = conn |> post(Routes.payee_path(conn, :create, params))
assert response.status == 400
end
and my PayeeContext.check_for_required_fields
def check_for_required_fields(fields) do
Enum.all?(@required_fields, fn field -> Map.has_key?(fields, field) end)
end
What am I doing wrong?
If-statements are rare in Elixir: pattern matching is more idiomatic. The forking of execution flow can be hard to follow, and you'll find that "early returns" can be a bit confusing at best, or an anti-pattern at worst.
Consider refactoring your code to do the pattern match in the function arguments. Not only does this eliminate the need for your validation function (because the pattern match will only succeed if those keys are present), it allows you to define multiple clauses of the create/2
function. This effectively gives you 2 execution paths: one for cases when the key
and type
parameters are present, and another when they are not.
def create(conn, %{"key" => key, "type" => type}) do
validation_func = PayeeContext.validation_map(type)
case validation_func.(key) do
{:ok, _} ->
conn |> send_resp(200, "Request created successfully")
{:error, message} -> conn |> send_resp(400, message)
end
end
def create(conn, _) do
send_resp(conn, 400, "Please provide Key and Type parameters")
end
Remember: the more specific matches must go first -- execution will be dispatched to the first function where a match succeeds.