Search code examples
elixirphoenix-frameworkex-unit

Check status code with ExUnit failed, but actual REST test success


I'm writing a controller function where it will check for a condition ( keyword to be valid ) before either to render a json object or error object if failed.

router.ex

scope "/api", DongNghiaWeb do
    pipe_through :api
    scope "/tim_kiem" do
      get "/tu/:word"   , APIWordController, :search_word
[...]

word_controller.ex

def search_word(conn, %{"word" => word}) do
  conn
  |> check_invalid_keyword(word)
  |> render("search.json", words: word |> String.trim |> Words.suggest)
end

defp check_invalid_keyword(conn, keyword) do
  unless Words.keyword_valid?(String.trim(keyword)) do
    conn
    |> put_status(400)
    |> json(%{
      error: "Invalid keyword"
    })
  end
  conn
end

word_controller_test.ex

test "response error when word is not valid", %{conn: conn} do
  response = get(conn,api_word_path(conn, :search_word, "a3d"))
    |> json_response(400)
  assert response["error"] == "Invalid keyword"
end

When running mix test, the results will be like so :

** (RuntimeError) expected response with status 400, got: 200, with body: {"data":[]}

But when I try testing with a REST client ( Insomnia, for example ), the json will return to be { error : "Invalid keyword" } just fine.


Solution

  • Your code is writing the response to the connection twice when Words.keyword_valid?(String.trim(keyword)) is falsy.

    The first write happens when you call |> json(...) and the second one when you call render. Your code does not prevent render being called when json has already been called. The browser connection ends after the first write, so you see the right output with Insomnia but the testing setup uses the last response written to the connection.

    Fixing this requires a bit of restructuring of your code. Here's how I would do it:

    def search_word(conn, %{"word" => word}) do
      if Words.keyword_valid?(String.trim(word)) do
        conn
        |> render("search.json", words: word |> String.trim() |> Words.suggest())
      else
        conn
        |> put_status(400)
        |> json(%{
          error: "Invalid keyword"
        })
      end
    end
    

    Edit: here's one way to do what you requested in the comment below:

    def search_word(conn, %{"word" => word}) do
      with {:ok, conn} <- check_invalid_keyword(conn, word) do
        conn
        |> render("search.json", words: word |> String.trim() |> Words.suggest())
      end
    end
    
    def check_invalid_keyword(conn, keyword) do
      if Words.keyword_valid?(String.trim(keyword)) do
        {:ok, conn}
      else
        conn
        |> put_status(400)
        |> json(%{
          error: "Invalid keyword"
        })
      end
    end
    

    When the keyword is invalid, the return value fails to match the with clause and is returned as is. If it was valid, the do block gets executed.