Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Elixir: How to test Phoenix Controllers when they need records in the database? With seeds or mocks?

When testing controllers it can be useful to have some data in the test database. And sometimes you might want to test the data creation. What is the correct way to set this up? A test for controllers should test if the create function of the controller is working, not the model.

As an example I want to test a Session controller and I have two tests. One is testing that a created user can login. The other that he cannot if the password is wrong. Both rely on a user being in the database. How I deal with it now is to create the user first:

defmodule MyApp.SessionControllerTest do
  use MyApp.ConnCase

  alias MyApp.Admin
  @valid_attrs %{email: "[email protected]", name: "John Doe", password: "goodpassword", password_confirmation: "goodpassword", password_hash: "somecontent", username: "username"}
  @invalid_attrs %{}

  setup do
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  test "admin can login after creation" do
    conn = post conn, admin_path(conn, :create), admin: @valid_attrs
    body = json_response(conn, 201)
    assert Repo.get_by(Admin, email: @valid_attrs[:email])
    conn = post conn, session_path(conn, :create), %{data: %{attributes: %{email: @valid_attrs[:email], password: @valid_attrs[:password]}}}
    body = json_response(conn, 201)
    assert body["data"]["token"]
  end

  test "login with wrong password returns an error" do
    conn = post conn, session_path(conn, :create), %{data: %{attributes: %{email: @valid_attrs[:email], password: "wrongpassword"}}}
    body = json_response(conn, 403)
    assert body["error"]
  end

end

If I now add a uniqueness restrain on my Admin model this could potentially get messy since whenever I need a user in the database I have to make sure that the test isn't failing because of this constraint but because something in the tested controller is wrong. Also it isn't clear in which order the tests are run and staying consistent with the data creation over several tests seems like a nightmare.

I either want one place where I define in the beginning which Data is created. Or use mocks for Controller testing.

How is this possible?

like image 938
Ole Spaarmann Avatar asked Dec 14 '22 06:12

Ole Spaarmann


2 Answers

Use the setup function for add some data to your database.

setup do
  Repo.insert!(%User{id: 1, password: "somepassword", ....})
  ....
  :ok
end

The setup will be called before every test. See ExUnit hexdocs for more information.

For run the tests step by step in synchronous mode add async: false to use call.

defmodule MyApp.SessionControllerTest do
  use MyApp.ConnCase, async: false`

But I think it's better to run the tests without dependencies to other tests.

defmodule MyApp.SessionControllerTest do
  use MyApp.ConnCase

  alias MyApp.Admin
  @valid_attrs %{email: "[email protected]", name: "John Doe", password: "goodpassword", password_confirmation: "goodpassword", password_hash: "somecontent", username: "username"}
  @invalid_attrs %{}

  setup do
    Repo.insert!(%User{email: "[email protected]", password: "somepassword", ....})
    {:ok, conn: put_req_header(conn, "accept", "application/json")}
  end

  ...

  test "login with wrong password returns an error" do
    conn = post conn, session_path(conn, :create), %{data: %{attributes: %{email: "[email protected]", password: "wrongpassword"}}}
    body = json_response(conn, 403)
    assert body["error"]
  end

end

Hope it helps.

like image 106
Fabi755 Avatar answered Feb 27 '23 16:02

Fabi755


Little bit late to the party but it might help others.

I would suggest using a ex_machina factory library. https://github.com/thoughtbot/ex_machina With it you can define and create test data in one place.

Define factories like so:

defmodule MyApp.Factory do
  # with Ecto
  use ExMachina.Ecto, repo: MyApp.Repo

  # without Ecto
  use ExMachina

  def user_factory do
    %MyApp.User{
      name: "Jane Smith",
      email: sequence(:email, &"email-#{&1}@example.com"),
    }
  end

  def article_factory do
    %MyApp.Article{
      title: "Use ExMachina!",
      # associations are inserted when you call `insert`
      author: build(:user),
    }
  end

  def comment_factory do
    %MyApp.Comment{
      text: "It's great!",
      article: build(:article),
    }
  end
end

And creating a record would be:

insert(:comment, article: article)

You can also build a record (unsaved)

build(:user)
like image 30
A. Maly Avatar answered Feb 27 '23 15:02

A. Maly