Testing Phoenix contexts without repeating changeset tests

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.

2 Likes

If you are testing the changeset directly elsewhere, your context test only needs to test what is unique to the context function. I’d say that means you test the record is added to the database with valid data, and isn’t with invalid data. You only need one instance of invalid data since you are covering the expected functionality elsewhere.

Personally I don’t see a whole lot of value in testing purely declarative code; so I don’t try to test everything a changeset does and typically only test the context function.

Thank you very much for your help. I will try to apply it.