I am currently working on a medium size Phoenix project, but still in the early stages. I have basic authentication and user accounts implemented, but as I started testing my contexts I ran into a problem. I have a context called Accounts which contains, amongst other code, the following two functions:
def create_user(attrs \\ %{}) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
def register_user(attrs \\ %{}) do
%User{}
|> User.registration_changeset(attrs)
|> Repo.insert()
end
Part of this context is Accounts.User which has the following changesets:
def changeset(user, attrs) do
user
|> cast(attrs, [:email, :username])
|> validate_required([:email, :username])
|> unique_constraint(:email)
|> validate_format(:email, @email_pattern)
|> validate_length(:username, min: 2, max: 20)
|> validate_format(:username, @username_pattern)
end
def registration_changeset(user, params) do
user
|> changeset(params)
|> cast(params, [:password])
|> validate_required([:password])
|> validate_length(:password, min: 8, max: 20)
|> put_pass_hash()
end
As you can see, registration_changeset/2 is just an extension of changeset/2.
My tests for register_user/1 are:
describe "register_user/1" do
@valid_attrs %{
username: "someuser",
email: "someuser@example.com",
password: "verysecret"
}
@invalid_attrs %{}
test "inserts user with valid data" do
assert {:ok, %User{id: id} = user} = Accounts.register_user(@valid_attrs)
assert user.username == "someuser"
assert user.email == "someuser@example.com"
assert [%User{id: ^id}] = Accounts.list_users()
end
test "does not insert user with invalid data" do
assert {:error, _changeset} = Accounts.register_user(@invalid_attrs)
assert Accounts.list_users() == []
end
test "enforces unique emails" do
assert {:ok, %User{id: id} = user} = Accounts.register_user(@valid_attrs)
assert {:error, changeset} = Accounts.register_user(@valid_attrs)
assert %{email: ["has already been taken"]} = errors_on(changeset)
assert [%User{id: ^id}] = Accounts.list_users()
end
test "declines invalid email" do
invalid_emails = [
"invalidemail",
"invalid@",
"@example.com",
"invalid@example",
"invalid@example.",
"invalid@example.c",
"invalid@example.verylong",
]
for email <- invalid_emails do
attrs = Map.put(@valid_attrs, :email, email)
assert {:error, changeset} = Accounts.register_user(attrs)
assert %{email: ["has invalid format"]} = errors_on(changeset)
assert [] == Accounts.list_users()
end
end
test "declines too long username" do
attrs = Map.put(@valid_attrs, :username, String.duplicate("a", 30))
assert {:error, changeset} = Accounts.register_user(attrs)
assert %{username: ["should be at most 20 character(s)"]} = errors_on(changeset)
assert [] == Accounts.list_users()
end
test "declines too short username" do
attrs = Map.put(@valid_attrs, :username, "a")
assert {:error, changeset} = Accounts.register_user(attrs)
assert %{username: ["should be at least 2 character(s)"]} = errors_on(changeset)
assert [] == Accounts.list_users()
end
test "declines username with spaces" do
attrs = Map.put(@valid_attrs, :username, "mary wozniak")
assert {:error, changeset} = Accounts.register_user(attrs)
assert %{username: ["has invalid format"]} = errors_on(changeset)
assert [] == Accounts.list_users()
end
end
Here you can see that I am effectively testing whether the changeset’s validation is working. This is problematic if I start testing the create_user/1 function, because it also uses the changeset and I will have to repeat the validation tests.
So my question is: How can I test the two context functions in a way that I do not repeat the code that tests if the changeset validations work?
I am new to Elixir and Phoenix and any help, examples and explanations are appreciated.