I’m trying to find a way to fix a design issue where I have some module methods public for the sole purpose of running tests on them.
I have a module that creates a user along with a confirmation token (which is stored in users
table field with uniqueness constraint on it) using these steps:
- generate a token with the help of
EmailConfirmationHandler
- merge it with passed user attributes
- persist the user
Because the token uniqueness validation happens on a user persisting step, there can be a case when the saving fails and the whole process needs to be repeated with a regenerated token.
My main module has only 1 method call
that I want to expose, but in order to test all scenarios properly I need to also test private methods(as I need to check how module performs with different results from EmailConfirmationHandler
), hence I change their accessibility level to public.
So the question is, is there a better way to design this functionality so I don’t have any private methods exposed for the sole purpose of testing?
Any feedback is highly appreciated. Thanks!
Main module
defmodule App.UserCreator do
@moduledoc """
Used for creating user with specified params
"""
alias App.{Accounts.EmailConfirmationHandler, Accounts.User, MapUtils, Repo}
def call(attrs) do
attrs
|> merge_confirmation_attrs
|> create_user_with
|> process_user_creation_result(attrs)
end
def merge_confirmation_attrs(attrs) do
Map.merge(MapUtils.atomize_keys(attrs),
EmailConfirmationHandler.generate_confirmation_params())
end
def create_user_with(attrs) do
%User{}
|> User.create_changeset(attrs)
|> Repo.insert()
end
def process_user_creation_result(result, attrs) do
case result do
{:ok, user} -> {:ok, user}
{:error, changeset} -> if confirmation_token_error_present?(changeset),
do: call(attrs),
else: {:error, changeset}
end
end
defp confirmation_token_error_present?(%Ecto.Changeset{errors: errors}) do
List.keymember?(errors, :confirmation_token, 0)
end
end
Tests
defmodule UserCreatorTest do
use App.DataCase
alias App.Accounts
alias App.UserCreator
@user_attrs %{email: "fred@example.com",
password: "reallyHard2gue$$"}
def fixture(:user, attrs \\ @user_attrs) do
{:ok, user} = Accounts.create_user(attrs)
user
end
test "create_user/1 with valid data creates a user" do
{:ok, user} = UserCreator.call(@user_attrs)
assert user.email == "fred@example.com"
assert user.confirmation_token
assert user.confirmation_sent_at
end
test "merge_confirmation_attrs/1 merges user attrs with generated confirmation attrs" do
result = UserCreator.merge_confirmation_attrs(%{email: "test@volocare.com"})
assert result.email == "test@volocare.com"
assert result.confirmation_sent_at
assert String.length(result.confirmation_token) == 8
end
test "create_user_with/1 creates user with passed attrs" do
attrs = %{email: "test@volocare.com",
password: "test1234",
confirmation_token: "just a random string"}
{:ok, user} = UserCreator.create_user_with(attrs)
assert user.email == "test@volocare.com"
assert user.confirmation_token == "just a random string"
end
test "process_user_creation_result/1 returns passed result whe it is successful" do
user = fixture(:user)
assert UserCreator.process_user_creation_result({:ok, user}, %{}) == {:ok, user}
end
test "process_user_creation_result/1 recreates user with new confirmation attrs when confirmation_token in invalid" do
user1 = fixture(:user)
user2_attrs = %{email: "test@volocare.com", password: "test1234"}
failed_creation = Map.merge(user2_attrs, %{confirmation_token: user1.confirmation_token})
|> UserCreator.create_user_with()
{:ok, result} = UserCreator.process_user_creation_result(failed_creation, user2_attrs)
assert result.email == "test@volocare.com"
refute result.confirmation_token == user1.confirmation_token
end
test "process_user_creation_result/1 returns passed result whe it is unsuccessful not because of confirmation_token" do
fixture(:user)
failed_creation = UserCreator.create_user_with(@user_attrs)
{:error, result} = UserCreator.process_user_creation_result(failed_creation, %{})
assert List.keymember?(result.errors, :email, 0)
end
end