View Source GitHub.Testing (GitHub REST API Client v0.1.1)

Support for interacting with the client in a test environment

Note

The current method of tracking and mocking API calls uses the process dictionary for storage. This means that tests can be run async (calls from one process will not affect calls from another). However, it also means that calls from an async task will not be tracked by the test process. If this affects you, please open an issue to discuss possible solutions.

usage

Usage

The testing facility provided by this library has two parts: this module, which provides helpful functions for generating data and asserting that API calls are made, and GitHub.Plugin.TestClient, which provides a basic mock and support for the test assertions.

In your test environment, you likely want to use the test client plugin as the only item in the client stack:

# config/test.exs
config :oapi_github, stack: [{GitHub.Plugin.TestClient, :request, []}]

The stack can also be configured at runtime by passing the :stack option to any operation.

Warning

Stack entries without options, like {GitHub.Plugin.TestClient, :request}, look like keyword list items. If you have stacks configured in multiple Mix environments that all use this 2-tuple format, Elixir will try to merge them as keyword lists. Adding an empty options element to any stack item will prevent this behaviour.

Then, in your test file, use this module and make use of the available assertions:

defmodule MyApp.MyTest do
  use ExUnit.Case
  use GitHub.Testing

  test "something" do
    my_function()
    assert_gh_called &GitHub.Repos.get/2
  end
end

The use macro also imports mock_gh/3 and generate_gh/2 for use in tests.

Link to this section Summary

Functions

Assert the number of times an API endpoint was called

Generate random data for use in a test response

Generate a struct for use in a test response

Mock a response for an API endpoint

Link to this section Functions

Link to this macro

assert_gh_called(call, opts \\ [])

View Source (macro)

Assert the number of times an API endpoint was called

The API endpoint can be passed as a function call or using function capture syntax. If passed as a function call, the arguments must match exactly or use the special value :_ to match any value. If passed using function capture syntax, only the arity will be matched. The options argument is not considered during these checks.

examples

Examples

assert_gh_called GitHub.Repos.get("owner", "repo")
assert_gh_called GitHub.Repos.get("owner", :_), times: 2
assert_gh_called GitHub.Repos.get(owner, "repo"), min: 2, max: 3
assert_gh_called &GitHub.Repos.get/2, times: 0

options

Options

  • max: Non-negative integer representing the maximum number of times a matching call should have occurred. If unspecified, there is no upper limit on the acceptable number of calls. If none of times, min, or max are specified, the default is to assert at least one matching call.

  • min: Non-negative integer representing the minimum number of times a matching call should have occurred. If unspecified, there is no lower limit on the acceptable number of calls. If none of times, min, or max are specified, the default is to assert at least one matching call.

  • times: Non-negative integer number of times a matching call should have occurred. Passing zero will assert the endpoint has not been called. This option has precedence over min and max. If none of times, min, or max are specified, the default is to assert at least one matching call.

Link to this function

generate(schema, key, type)

View Source
@spec generate(module(), atom(), GitHub.Operation.type()) :: any()

Generate random data for use in a test response

Data generated by this function will appear as if it were decoded by a plugin (for example GitHub.Plugin.TypedDecoder). Including such a decoder in the stack is not necessary when the client uses this data.

This function uses randomness to help avoid collisions in tests. It also supports special cases based on the context of the generated data (the schema and/or key). Examples can be found in the Special Cases section below.

special-cases

Special Cases

  • :id fields with type :integer will have unique positive integers rather than the limited range of integers returned by a typical :integer field.

  • :string fields with keys that end with _at will have ISO 8601 datetime strings from a random time within the past day.

  • :string fields with keys named :url or that end with _url will have random URL strings.

Additional special cases can be added to make the generated data more typical of real responses. For example, returning a name-like string for user names, and limiting the allowed characters for usernames.

Note

If you see an opportunity to improve the generated data for a particular field, please include the description of the field from GitHub's OpenAPI specification in your pull request.

examples

Examples

iex> GitHub.Testing.generate(nil, nil, {GitHub.Repository, :full})
%GitHub.Repository{...}

iex> GitHub.Testing.generate(GitHub.Repository, :id, :integer)
226194162
Link to this function

generate_gh(schema, type \\ :t, overrides \\ %{})

View Source
@spec generate_gh(module(), atom(), map() | keyword()) :: any()

Generate a struct for use in a test response

The first argument is the module / schema you would like to generate. If there are multiple types available in the module, then the second argument can distinguish which to use (for example, :full for the type GitHub.PullRequest.full()). It is also possible to override the generated fields with custom data.

This function uses randomness to help avoid collisions in tests. For more information, see generate/3.

examples

Examples

iex> GitHub.Testing.generate_gh(GitHub.PullRequest)
%GitHub.PullRequest{}

iex> GitHub.Testing.generate_gh(GitHub.User, :private)
%GitHub.User{}

iex> GitHub.Testing.generate_gh(GitHub.User, bio: "This is a custom bio")
%GitHub.User{bio: "This is a custom bio"}

iex> GitHub.Testing.generate_gh(GitHub.User, :private, bio: "This is a custom bio")
%GitHub.User{bio: "This is a custom bio"}
Link to this macro

mock_gh(call, return_fn, opts \\ [])

View Source (macro)

Mock a response for an API endpoint

api-endpoint

API Endpoint

The API endpoint can be passed as a function call or using function capture syntax. If passed as a function call, the arguments must match exactly or use the special value :_ to match any value. If passed using function capture syntax, only the arity will be matched. The options argument is not considered during these checks.

return-value

Return Value

As a return value for a mock, you can set a plain value or pass one of several function forms:

  • A zero-arity function will be evaluated a call time. Use this to perform lazy evaluation of the mock or encapsulate a generator.

  • A function with the same number of arguments as the original client operation (not including the final opts argument) will be called with the same arguments. Use this to perform assertions on the arguments or return a different value depending on the call.

  • A function with the same number of arguments as the original client operation (including the final opts argument) will be called with the same arguments and options. Use this to perform assertions on the arguments and options or return a different value depending on the call.

In each case, the plain value — or the value returned from the function — should have one of the following forms:

{:ok, data}
{:ok, data, opts}
{:error, error}
{:error, error, opts}

Where data is the response body and error is the error to return, and opts modifies the response. The available options are:

  • code (integer): Status code to include with the response.

In addition, the following pre-defined error responses are available:

  • {:error, :not_found} will return an error matching GitHub's standard "Not Found" response.
  • {:error, :rate_limited} with return an error matching a GitHub API rate limited response.
  • {:error, :unauthorized} will return an error matching GitHub's unauthorized response.

examples

Examples

mock_gh GitHub.Repos.get("owner", "repo"), {:ok, %GitHub.Repository{}}
mock_gh GitHub.Repos.get("owner", "repo-2"), {:ok, %GitHub.Repository{}, code: 201}
mock_gh GitHub.Repos.get("friend", "repo"), {:error, :not_found}
mock_gh GitHub.Repos.get("friend", "repo-2"), {:error, %GitHub.Error{}, code: 403}

mock_gh &GitHub.Repos.get/2, fn -> {:ok, %GitHub.Repository{}} end

mock_gh &GitHub.Repos.get/2, fn owner, name ->
  assert String.starts_with?(name, "oapi_")
  {:ok, %GitHub.Repository{owner: owner, name: name}}
end

mock_gh &GitHub.Repos.get/2, fn owner, name, opts ->
  assert opts[:auth] == "gho_token"
  {:ok, %GitHub.Repository{owner: owner, name: name}}
end