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
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 oftimes
,min
, ormax
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 oftimes
,min
, ormax
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 overmin
andmax
. If none oftimes
,min
, ormax
are specified, the default is to assert at least one matching call.
@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
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"}
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