View Source MockGRPC (MockGRPC v0.2.0)

MockGRPC is a library for defining concurrent client mocks for gRPC Elixir.

It works by implementing a client adapter that intercepts requests using the mocks you defined, and performing expectations on them.

usage

Usage

Imagine that you have a module calling a say_hello RPC.

defmodule Demo do
  def say_hello(name) do
    {:ok, channel} = GRPC.Stub.connect("localhost:50051")
    GreetService.Stub.say_hello(channel, %SayHelloRequest{name: "John Doe"})
  end
end

The first step is to change the connect code to use an adapter coming from the app environment, so that you can use MockGRPC in test mode, and the default adapter in dev and production.

{:ok, channel} =
  GRPC.Stub.connect(
    "localhost:50051",
    adapter: Application.get_env(:demo, :grpc_adapter)
  )

Or if you're using ConnGRPC, add adapter to the channel opts.

Then, on your config/test.exs, set it to MockGRPC.Adapter:

Application.put_env(:demo, :grpc_adapter, MockGRPC.Adapter)

Now it's time to write your test. To enable mocks, add use MockGRPC to your test, and call MockGRPC.expect/2 or MockGRPC.expect/3 to set expectations.

defmodule DemoTest do
  use ExUnit.Case, async: true

  use MockGRPC

  test "say_hello/1" do
    MockGRPC.expect(&GreetService.Stub.say_hello/2, fn req ->
      assert %SayHelloRequest{name: "John Doe"} == req
      {:ok, %SayHelloResponse{message: "Hello John Doe"}}
    end)

    assert {:ok, %SayHelloResponse{message: "Hello John Doe"}} = Demo.say_hello("John Doe")
  end
end

All expectations are defined based on the current process. This means that if you call gRPC from a separate process, and this process is not a Task*, it won't have access to the expectations by default. But there are ways to overcome that. See the "Multi-process collaboration" section.

*Task is supported automatically with no extra code needed due to its native caller tracking.

multi-process-collaboration

Multi-process collaboration

MockGRPC supports multi-process collaboration via two mechanisms:

  1. manually set context
  2. global mode

manually-set-context

Manually set context

In order for other processes to have access to your mocks, you can call set_context/1 on the external process passing the PID of the test.

global-mode

Global mode

To support global mode, set your test async option to false. However, this won't allow your test file to execute in parallel with other tests.

Link to this section Summary

Functions

Makes GRPC.Stub.connect/2 return an error tuple, simulating that the server is down.

Adds an expectation using a gRPC service function capture.

Adds an expectation using the gRPC service module and function name.

Set mock context, in case the mock is being called from another process in an async test.

Makes GRPC.Stub.connect/2 return successfully, reverting the down/0 call.

Link to this section Functions

Makes GRPC.Stub.connect/2 return an error tuple, simulating that the server is down.

If you're using ConnGRPC, you will need additional code to make it work. See ConnGRPC - Simulating unavailable channel

Link to this function

expect(grpc_fun, mock_fun)

View Source

Adds an expectation using a gRPC service function capture.

Example:

MockGRPC.expect(&GreetService.Stub.say_hello/2, fn req ->
  assert %SayHelloRequest{name: "John Doe"} == req
  {:ok, %SayHelloResponse{message: "Hello John Doe"}}
end)

assert {:ok, %SayHelloResponse{message: "Hello John Doe"}} = Demo.say_hello("John Doe")
Link to this function

expect(service_module, fun_name, mock_fun)

View Source

Adds an expectation using the gRPC service module and function name.

Example:

MockGRPC.expect(GreetService.Service, :say_hello, fn req ->
  assert %SayHelloRequest{name: "John Doe"} == req
  {:ok, %SayHelloResponse{message: "Hello John Doe"}}
end)

assert {:ok, %SayHelloResponse{message: "Hello John Doe"}} = Demo.say_hello("John Doe")

Set mock context, in case the mock is being called from another process in an async test.

This is not needed for Task processes.

Example:

test "calling a mock from a different process" do
  parent = self()

  MockGRPC.expect(&GreetService.Stub.say_hello/2, fn req ->
    assert %SayHelloRequest{name: "John Doe"} == req
    {:ok, %SayHelloResponse{message: "Hello John Doe"}}
  end)

  # This is just an example to demonstrate the concept. In a real world scenario, you'd
  # be better off using the `Task` module, which doesn't require calling `set_context`
  spawn(fn ->
    # Ensure this process has access to the mocks
    MockGRPC.set_context(parent)

    {:ok, channel} = GRPC.Stub.connect("localhost:50051", adapter: Application.get_env(:demo, :grpc_adapter))
    response = GreetService.Stub.say_hello(channel, %SayHelloRequest{name: "John Doe"})

    # Do the assertion outside the process, to avoid a race condition where the test
    # finishes before this process completes execution.
    send(parent, {:my_process_result, response})
  end)

  assert_receive {:my_process_result, %SayHelloResponse{message: "Hello John Doe"}}
end

Makes GRPC.Stub.connect/2 return successfully, reverting the down/0 call.