Reason Native
  • Docs
  • Help
  • GitHub

›Rely

Overview

  • Introduction
  • Native Basics
  • Getting Started

Rely

  • Introduction
  • Quickstart
  • Guides

    • Setup and Teardown
    • Mock Functions

    API Reference

    • Basic API
    • Expect
  • Advanced

Console

  • Introduction
  • Quickstart
  • API

Pastel

  • Introduction
  • Quickstart
  • API
  • Console

Refmterr

  • Introduction
  • Quickstart

File Context Printer

  • Introduction
  • Quickstart
  • API

Mock Functions

Mock functions (or "spies") in Rely can be used to track information about the arguments and return values of functions, and allow for test-time configuration of return values. Rely has built-in matchers for dealing with mock functions that are documented extensively here.

Unlike Jest in JavaScript, module level mocking is not supported. The functionality available in Rely is analagous to what Jest offers via jest.fn and there is no direct jest.mock analogy. Many situations in which jest.mock would be useful can be addressed through dependency management and the structuring of side effects within your application. A simple example using functors is shown below.

Using Mock Functions

Creating Mocks

There are constructors for creating mocks that take up to 7 arguments. A default implementation must be specified.

open TestFramework;

let calculateInterest = Mock.mock3((p, r, t) => p *. exp(r *. t));
module Mock: {
  type t('fn, 'returnType, 'tupledArgs);

  let mock1: ('args => 'ret) => t('args => 'ret, 'ret, 'args);

  let mock2:
    (('arg1, 'arg2) => 'ret) =>
    t(('arg1, 'arg2) => 'ret, 'ret, ('arg1, 'arg2));

  let mock3:
    (('arg1, 'arg2, 'arg3) => 'ret) =>
    t(('arg1, 'arg2, 'arg3) => 'ret, 'ret, ('arg1, 'arg2, 'arg3));

  let mock4:
    (('arg1, 'arg2, 'arg3, 'arg4) => 'ret) =>
    t(
      ('arg1, 'arg2, 'arg3, 'arg4) => 'ret,
      'ret,
      ('arg1, 'arg2, 'arg3, 'arg4),
    );

  let mock5:
    (('arg1, 'arg2, 'arg3, 'arg4, 'arg5) => 'ret) =>
    t(
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5) => 'ret,
      'ret,
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5),
    );

  let mock6:
    (('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6) => 'ret) =>
    t(
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6) => 'ret,
      'ret,
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6),
    );

  let mock7:
    (('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'arg7) => 'ret) =>
    t(
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'arg7) => 'ret,
      'ret,
      ('arg1, 'arg2, 'arg3, 'arg4, 'arg5, 'arg6, 'arg7),
    );
};

Calling Mock Functions

The Mock.mockN constructor returns a mutable opaque type for tracking call information. To access the underlying function, use Mock.fn.

open TestFramework;

describe("Mock documentation", ({test}) => {
  test("calling a mock function", ({expect}) => {
    let mockDouble = Mock.mock1(x => x * 2);
    let mockFunction = Mock.fn(mockDouble);

    expect.int(mockFunction(2)).toBe(4);
  })
});

Injecting via Functor

When testing code with side effects such as talking to a database or making network requests, it is often desirable to be able to mock out these side effects. A straightforward way of achieving this is by passing functions that perform side effects as arguments. However, this is not always desirable due to the problem of threading such arguments through deeply nested code. The use of module functions (functors) can accomplish a similar thing and somewhat alleviate this issue.

For example, suppose that we have some code that uses a logger. In production we want the logger to make HTTP requests to some endpoint, but we deem this behavior undesirable for our test.

Without abstracting away the dependendency on our HTTP logger, our code might look like this.

MyModule.re

module MyApp = {
  let doSomethingThatGetsLogged = () => {
    HTTPLogger.log("starting to do someting");
    SomeModule.doSomething();
    HTTPLogger.log("did the thing!");
  };
};

Instead we could inject the code via a functor (module function) or via an ordinary function.

MyModule.re

module Make = (Dependencies: {let log: string => unit;}) => {
  let doSomethingThatGetsLogged = () => {
    Dependencies.log("starting to do something");
    SomeModule.doSomething();
    Dependencies.log("did the thing!");
  };
};
/*
 * Making the "real" versions of the module could easily be handled elsewhere
 *  in the application but is shown here for simplicity
 */
include Make({
  /*
   * Generally it would be better practice to have some Logger module type and
   * specify that there is a logger module in the functor signature, but for the
   * simplicity of this example we use a function instead
   */
  let log = HTTPLogger.log;
});

In your tests you can do this

MyModuleTest.re

open TestFramework;

let mockLog = Mock.mock1(_ => ());
module MyModule =
  RelyTalk.MyModule.Make({
    let log = Mock.fn(mockLog);
  });

let {describe} =
  describeConfig
  |> withLifecycle(testLifecycle =>
       testLifecycle |> beforeEach(() => Mock.reset(mockLog))
     )
  |> build;

describe("MyModule", ({test}) => {
  test("Should log a start message", ({expect}) => {
    MyModule.doSomethingThatGetsLogged();
    expect.mock(mockLog).toBeCalledWith("starting to do something");
  });
  test("should log a completion message", ({expect}) => {
    MyModule.doSomethingThatGetsLogged();
    expect.mock(mockLog).toBeCalledWith("did the thing!");
  });
});

API reference

Mock.changeImplementation(implementation, mock)

Accepts an implementation to be used for calls to the underlying mock function. The original implementation passed to the Mock constructor can be restored via Mock.reset or Mock.resetImplementation.

open TestFramework;

describe("Mock.changeImplementation", ({test}) => {
  test("changes function implementation", ({expect}) => {
    let mock = Mock.mock1(() => 42);
    let fn = Mock.fn(mock);
    expect.int(fn()).toBe(42);

    Mock.changeImplementation(() => 43, mock);

    expect.int(fn()).toBe(43);
  });
});

Mock.reset(mock)

Resets all information relating to stored calls and results, as well as restoring the mock to the original implementation it was constructed with.

Equivalent to calling both Mock.resetHistory and Mock.resetImplementation

Mock.resetHistory(mock)

Resets all information relating to stored calls and results.

Often this is useful when you want to clean up a mock's usage data between two assertions.

Mock.resetImplementation(mock)

Restores the original mock implementation to what it was first constructed with, regardless of how many times Mock.changeImplementation has been called.

Mock.getCalls(mock)

Returns a list of the arguments that have been passed to the underlying function. The calls are ordered from most to least recent. Generally the built in mock matchers should be used instead of Mock.getCalls.

Mock.getResults(mock)

Returns a list of Mock.result from the underlying function. The calls are ordered from most to least recent. Generally the built in mock matchers should be used instead of Mock.getResults.

module Mock = {
  type result('a) =
    | Return('a)
    | Exception(exn, option(Printexc.location), string);
};
← Setup and TeardownBasic API →
  • Using Mock Functions
    • Creating Mocks
    • Calling Mock Functions
    • Injecting via Functor
  • API reference
    • Mock.changeImplementation(implementation, mock)
    • Mock.reset(mock)
    • Mock.resetHistory(mock)
    • Mock.resetImplementation(mock)
    • Mock.getCalls(mock)
    • Mock.getResults(mock)
Reason Native
Projects
RelyConsolePastelRefmterrFile Context Printer
More
GitHubStar
Facebook Open Source
Copyright © 2021 Facebook Inc.