** (Mox.VerificationError) error while verifying mocks for #PID<0.534.0>: * expected DirectHomeApi.Aws.MockS3.upload_files/1 to be invoked once but it was invoked 0 times

Hello guys I’m trying mock a function that is executed when I call my endpoint (In the middle of all code).

But when I call my endpoint all is working but th mock not is called and upload the file to s3. Can someone give me a help with this?

Thanks! :smiley:

This is the error

  1) test upload user image when the user is logged (DirectHomeApiWeb.UserControllerTest)
     test/direct_home_api_web/controllers/user_controller_test.exs:165
     ** (Mox.VerificationError) error while verifying mocks for #PID<0.534.0>:
     
       * expected DirectHomeApi.Aws.MockS3.upload_files/1 to be invoked once but it was invoked 0 times
     stacktrace:
       (mox 1.0.0) lib/mox.ex:692: Mox.verify_mock_or_all!/3
       (ex_unit 1.11.3) lib/ex_unit/on_exit_handler.ex:143: ExUnit.OnExitHandler.exec_callback/1
       (ex_unit 1.11.3) lib/ex_unit/on_exit_handler.ex:129: ExUnit.OnExitHandler.on_exit_runner_loop/0

user_controller_test.ex


defmodule DirectHomeApiWeb.UserControllerTest do
  use DirectHomeApiWeb.ConnCase
  use ExUnit.Case, async: true

  import Mox

  alias DirectHomeApi.Model.User
  alias DirectHomeApi.Repo


  doctest DirectHomeApi.Aws.S3

  setup :verify_on_exit!
  describe "upload user image" do
    test "when the user is logged", %{conn: conn} do
      DirectHomeApi.Aws.MockS3
      |> expect(:upload_files, fn _image -> {:ok, %{}} end)

      image = %Plug.Upload{
        content_type: "image/jpeg",
        filename: "test.jpeg",
        path: "test/images/some_image.jpeg"
      }

      user = create_user()

      conn =
        sigin_and_put_token(conn, user)
        |> post(Routes.user_path(conn, :upload_image), %{"id" => user.id, "photo" => image})
      
      assert 200 = conn.status
      assert {:ok, %{"sucess" => "The image could be saved sucessfully"}} = Jason.decode(conn.resp_body)
      #%{"error" => "The image not could be storage in s3"}
    end
  end
end

s3.ex

defmodule DirectHomeApi.Aws.S3 do
  @callback upload_files(arg :: any) :: {:ok, map()} | {:ok, map()}

  def upload_files(image) do
    content_type = image.content_type
    format_file = String.split(content_type, "/") |> List.last()
    file_name = "#{Ecto.UUID.generate()}.#{format_file}"
    file_path = image.path
    bucket = System.get_env("BUCKET_NAME")

    ExAws.S3.put_object(bucket, file_name, File.read!(file_path), [
      {:content_type, content_type}
    ])
    |> ExAws.request()
    |> case do
      {:ok, _} -> {:ok, file_name}
      {:error, error} -> {:error, error}
    end
  end
end

test_helper.ex

ExUnit.start()
Ecto.Adapters.SQL.Sandbox.mode(DirectHomeApi.Repo, :manual)
Mox.defmock(DirectHomeApi.Aws.MockS3, for: DirectHomeApi.Aws.S3)

1 Like

Mock reported that mocked function was not called.

From this code, it seems that you are calling the wrong controller. Do you have controller with function named upload_image? Try to replace upload_image with upload_files

1 Like

Also you don’t show the controller code… The code that calls S3.upload_files. Are you swapping out the real module for the mock? Defining the mock is only half the solution. You need to also set up test.exs to put the mock in the env and make the controller get the S3 module from the application env instead of using the module directly. Hope that helps!

1 Like

Yes this is the controller

defmodule DirectHomeApiWeb.UserController do
  @callback upload_files(arg :: any) :: {:ok, map()} | {:ok, map()}
  use DirectHomeApiWeb, :controller

  alias DirectHomeApi.Model.User
  alias DirectHomeApi.CrudBase
  alias DirectHomeApiWeb.Auth.Guardian

  def upload_image(conn, %{"id" => id, "photo" => user_image}) do
    response = User.update_image(id, %{"photo" => user_image})

    case response do
      {:ok, body} -> json(conn, body)
      {:error, body} -> json(conn, body)
    end
  end
end

and this is the user module

user.ex

defmodule DirectHomeApi.Model.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias DirectHomeApi.Repo
  alias DirectHomeApi.Model.{User, Property}
  alias DirectHomeApi.Aws.S3
  alias DirectHomeApi.Errors.ErrorHandler

  @derive {Jason.Encoder, except: [:__meta__, :inserted_at, :updated_at, :password]}

  schema "users" do
    field :document, :integer
    field :document_type, :string
    field :email, :string
    field :last_name, :string
    field :name, :string
    field :password, :string
    field :phone, :integer
    field :photo, :string
    field :type, Ecto.Enum, values: [:admin, :client]
    has_many :properties, Property

    timestamps()
  end

  @doc false
  # Falta agregar validaciones de documento y sus tests
  def changeset(user, attrs) do
    user
    |> cast(attrs, [
      :name,
      :last_name,
      :phone,
      :email,
      :photo,
      :document,
      :document_type,
      :password,
      :type
    ])
  end

  def changeset_create(user, attrs) do
    changeset =
      changeset(user, attrs)
      |> validate_required([
        :name,
        :last_name,
        :email,
        :password,
        :type
      ])
      |> unique_constraint([:email])
      |> validate_format(:email, ~r/@/)
      |> unsafe_validate_unique([:email], Repo)
      |> validate_length(:password, min: 8)
      |> put_change(:password, Bcrypt.hash_pwd_salt(attrs["password"]))
  end

  def update_image(id, user_image) do
    S3.upload_files(user_image["photo"])
    |> case do
      {:ok, filename} ->
        changeset =
          Repo.get!(User, id)
          |> cast(%{"photo" => System.get_env("S3_URL") <> filename}, [:photo])

        case changeset.valid? do
          true ->
            Repo.update!(changeset)
            {:ok, %{"sucess" => "The image could be saved sucessfully"}}

          false ->
            {:error, ErrorHandler.changeset_error_to_map(changeset)}
        end

      {:error, _} ->
        {:error, %{"error" => "The image not could be storage in s3"}}
    end
  end
end

This endpoint is working correctly and I’ve mocked the module and I did unit test of this module (S3) and working correctly but when I call through the endpoint not is working the mock.

This test is working

defmodule DirectHomeApi.Aws.S3Test do
  import Mox
  use ExUnit.Case, async: true
  doctest DirectHomeApi.Aws.S3

  setup :verify_on_exit!

  describe "upload file to s3" do
    test "the file was upload correctly" do
      DirectHomeApi.Aws.MockS3
      |> expect(:upload_files, fn _image -> {:ok, %{}} end)

      image = %Plug.Upload{
        content_type: "image/jpeg",
        filename: "test.jpeg",
        path: "some_path"
      }

      assert DirectHomeApi.Aws.MockS3.upload_files(image) ==
               {:ok, %{}}
    end

    test "the file not was upload" do
      DirectHomeApi.Aws.MockS3
      |> expect(:upload_files, fn _image -> {:error, %{}} end)

      image = %Plug.Upload{
        content_type: "image/jpeg",
        filename: "test.jpeg",
        path: "some_path"
      }

      assert DirectHomeApi.Aws.MockS3.upload_files(image) ==
               {:error, %{}}
    end
  end
end

That’s what yukster pointed out, you are not swapping out the implementation in your user.ex.

The error message is telling you exactly that:

expected DirectHomeApi.Aws.MockS3.upload_files/1 to be invoked once but it was invoked 0 times

In your code you never made a call to MockS3.

You have a line like this:

 def update_image(id, user_image) do
    S3.upload_files(user_image["photo"])
...

You see how it directly calls S3? You need to inject the dependency here, so something like this instead:

 def update_image(id, user_image) do
    s3_provider().upload_files(user_image["photo"])
...
end

def s3_provider, do: Application.fetch_env!(:your_app, :s3_provider)

In your config you would need to have s3_provider be configured to the real S3 omdule, and in your test you would need to configure it to use your MockS3.

This line:

Mox.defmock(DirectHomeApi.Aws.MockS3, for: DirectHomeApi.Aws.S3)

This creates a mock for you based on the behaviour, but you still need to inject the dependency yourself

2 Likes

Thanks, @edisonywh! Yep, that’s exactly what I was getting at (but was replying on a tablet in the middle of the night with insomnia, so I didn’t give examples). Thank you for the explicit instructions.

I think the official docs could be better about this: Mox — Mox v1.0.0. It talks about putting the module being mocked into the app config but the example of using a mock doesn’t take that approach.

Basically, there’s no magic involved. Just defining a mock doesn’t make it be used. You have to make the real code use the real module and the test code use the mock.

Their example also uses it more like a stub, just returning a value, not pattern matching the args, and not asserting anything about how the mock was called. If I’m going to use expect then I like to assert something in the function body passed to it. Or at the very least, pattern match the params so that the test will fail if the mock isn’t called the right way.

Anyway, @bian2510 I hope all this helps and doesn’t confuse you even more. I definitely struggled a bit with Mox when I first adopted it. And it still bothers me some that I have to alter the production code in order to use a Mox mock. But that alteration is limited to just making a given module configurable via the application env so it’s worth it.

1 Like

Yes sure, now I could understand, this make sense, let me try and if I can make this work, I let you know. Thanks guys, excellent explanation. @edisonywh @yukster @karlosmid

Yes guys, this is the solution, thank you very much… @yukster @edisonywh