Stubbing and mocking data in Avers

Up until recently most of Avers required you to run a compatible API server. Even if it's not that difficult, it adds enough overhead to turn off people. Last week I added two new managed object types which make Avers useful in applications that don't have that server. But still, even if you run that server, there are situations when you want to avoid accessing it. For example to make tests run faster. Or when you want to prototype a new feature in the client, then it is often easier to start with dummy data and implement persistence later.

Avers now exports enough functions so you can intercept requests and simulate responses from the server. In this article I'll explain how to do both.

Intercept outgoing network requests

When working with Avers, the first thing we need is a Handle. Remember that the Handle abstracts access to the network, clock and other impure resources. That is where we can intercept outgoing requests. In the following example we create a new Handle which rejects all network requests (as if the server was down).

Reject all network requests
function fetch(url) {
    return Promise.reject(new Error("Failed request to " + url));
}

let aversH = new Avers.Handle(..., fetch, ...);

That is a good starting point for unit tests and other forms of tests where you want to improve performance by stubbing out access to the network. Rejecting the request is a rather graceful way to handle it. You could also make any attempt to access the network a fatal error. This is useful in situations where you want to make absolutely sure that the code which is tested does not even trigger network requests.

Make any access to the network a fatal error
function fetch(url) {
    throw new Error('Access to network denied (' + url + ')');
}

In certain situations you expect a network request to be sent out. For those cases you can mock a custom response. In the following example the Handle expects the first request to be made to /session, and it will return a custom JSON response.

Intercept first request and return custom response
function mkHandle(expectedUrl, json) {
    function fetch(url) {
        assert(url === expectedUrl);
        return Promise.resolve(json);
    }

    return new Avers.Handle(..., fetch, ...);
}

let aversH = mkHandle('/session', { objId: '1234' });

Now that we've covered how to intercept outgoing network requests, let's look at how to simulate server responses.

Simulate server responses

Avers exports the very same functions it uses internally to process server responses. The functions for resolving the three managed object types are named resolve{Editable,Static,Ephemeral}. Resolve here means roughly: OHAI, btw, I received a 2xx response from the server and here's the JSON.

For Editable<T> objects, the JSON payload is expected to have a particular structure. It includes the object Id, type (which determines how the content should be parsed) and the content. Once an Editable<T> is resolved, it will be available to the lookup functions.

Resolve an Editable
let objId = 'my-123-accId'
Avers.resolveEditable(aversH, objId,
    { id: objId
    , type: 'account'
    , content: { name: 'John Snow' }
    }
);

// ASSERT: Avers.lookupEditable(aversH, objId).get() !== undefined

The value of a Static<T> is opaque to Avers. To resolve it you need a way to identify it. This is done through the Static<T> object itself, as it contains both the namespace and nameā€”the two parts which are needed to identify it.

Resolve a Static
let apiVersionS = new Avers.Static(myNamespace, 'apiVersion', ...);
Avers.resolveStatic(aversH, apiVersionS, '1.3.7');

// ASSERT: Avers.lookupStatic(aversH, apiVersionS).get() === '1.2.3'

Resolving an Ephemeral<T> works similarly. But since an ephemeral object has a limited lifetime, you have to include an expiration time when resolving the object. The expiration time uses the same time scale as the now function that you have to supply when creating an Avers Handle. So often it makes sense to call the function that is stored in the Avers Handle and generate a timestamp that is relative to it.

Here we resolve an Ephemeral<T> with an expiration time one minute in the future.

Resolve an Ephemeral
let serverLoadE = new Avers.Ephemeral(myNamespace, 'serverLoad', ...);
Avers.resolveEphemeral(aversH, serverLoadE, 0.72, aversH.now() + 60 * 1000);

// ASSERT: Avers.lookupEphemeral(aversH, serverLoadE).get() === 0.72

You can also resolve the object with a expiration time that is already in the past. That will mark the object as stale, but since the value is available, it will be returned nonetheless when you look up the object value.

Note that when you lookup a stale ephemeral, Avers will immediately trigger a network request to refresh it. So make sure to stub out the fetch function.

Resolve an Ephemeral with a expiration time in the past
let serverLoadE  = new Avers.Ephemeral(myNamespace, 'serverLoad', ...);
let oneMinuteAgo = aversH.now() - 60 * 1000;

Avers.resolveEphemeral(aversH, serverLoadE, 0.72, oneMinuteAgo);

// ASSERT: Avers.lookupEphemeral(aversH, serverLoadE).get() === 0.72

Speed up the initial pageload

Filling an Avers Handle with data can also be used to speed up the initial pageload. If you include the object data in the HTML file, the application can load it into the handle and avoid sending out additional network requests.

<body>
    <script>
        window.editables =
            [ { objId: '4dar7q54d', type: 'account': content: { ... } }
            , { objId: 'oau09av4q', type: 'encounter': content: { ... } }
            , { objId: '45daa0awd', type: 'tile': content: { ... } }
            ];
    </script>

    <script>
        var aversH = new Avers.Handle(...);
        window.editables.forEach(function(json) {
            Avers.resolveEditable(aversH, json.objId, json);
        });
    </script>
</body>

Summary

We've seen how Avers generates network requests and which functions it uses to process the responses. All access to the network abstracted and users of the Avers library are in full control. They can intercept these requests and simulate custom responses. This is useful in tests which need to run in isolation or be decoupled from the server implementation.

In the second part I've introduced functions which Avers uses to process server responses, and how you can fill the Handle with custom data.