#
Mocking levels
When a request hits the mock server, it is crucial to understand how fields are resolved.
Example query request
graph LR A((Query)) --> B(Resolvers) B --> C(Store) C --> D(Mocks)
- Resolvers: First (and always) any resolvers are processed. If no resolver is defined for a given field, it will fallback to the default which returns a random value for the return type.
- Store: This holds all objects that have been returned by the mock server and acts as a cache. If the store already has a value for an instance field, that value will be returned. If the store doesn't have a value yet, it will use the mocks to generate one and cache it.
- Mocks: The store uses mocks to get values for fields. It will first try the specific field mock (such as
User.username
) then fallback to a Scalar type mock (such asString
) if undefined.
To understand the different levels of mocking, consider an example schema:
type User {
id: String!
username: String!
}
type Query {
userById(id: String!): User
}
This schema is actually implemented in @datacamp/poser-service-playground
.
All fields are "auto-mocked" by default and each field will produce a value based on the field's type. In our example, the type User
has two String
fields which will both be mocked using a random string.
#
Mocks
#
Scalar types
You may redefine defaults for scalar types such as String
and Integer
. This is the final fallback and if no value is provided by the resolvers or "type field" mocks, the mock server will use the corresponding "scalar type" mock.
Querying for User.id
or User.username
will return a random string. This comes from the defaults provided by Poser.
import { faker } from '@faker-js/faker';
import type { Mocks } from '../types';
const defaultMocks: Mocks = {
DateTime: () => faker.date.past().toISOString(),
Int: () => faker.datatype.number(),
JSON: () => faker.datatype.json(),
String: () => faker.random.words(4),
};
export default defaultMocks;
We can specify our own default for any scalar type by providing mocks
:
const mocks = {
String: () => 'All your strings are belong to us'
}
With this configuration, all fields of type String
will return "All your strings are belong to us"
.
#
Type fields
We can also redefine what data should be returned for specific fields. In the above example, we have a type User
with fields id
and username
which both fallback to String
.
To redefine the User.id
field:
const mocks = {
User: {
id: () => faker.datatype.string(7)
}
}
With this configuration, User.id
will always return a random string such as "fiy61g6"
.
A word on mocks
If you've followed the above examples and tried to make the same GrapqhQL query multiple times, you'll notice that all calls return the same random values (i.e. the same random values as the first call returned). This is because all data flows through the store.
Mocks can only provide values to the store when it doesn't have a value. Once the store receives a value from a mock, that value will be cached in the store until restart.
#
Resolvers
Mocks are great for returning random values for local development which is closer to reality. However, mocks are limited and cannot implement logic. To go further and perform more complex mocking, use resolvers
.
The first thing to be processed during a request are the resolvers. As we saw, mocks can only provide values to the store, but resolvers are fully in charge of what is returned. Resolvers may choose to query from the store or return entirely new values.
Resolvers look a lot like mocks, but behave very differently. Consider the same mock and resolver for User.name
:
const mocks = {
User: {
name: () => faker.name.firstName()
}
}
const resolvers = () => {
return {
User: {
name: () => faker.name.firstName()
}
}
}
The implementation is identical, but the results are different:
- when using a Mock, our
User.name
will be cached in the store and attached to user instances. - when using a Resolver, our
User.name
will be different every time we query it. This is because resolvers don't use the store, meaning the value is never cached.
Unlike mocks, resolver functions receive four arguments which are identical to an actual GraphQL server.
#
The store
The store can be seen as an object store (a bit like mongo or any NoSQL database), except it always has the object you're looking for. If it doesn't have the object you're looking for, it will create one (using mocks
to populate fields), return it and store it for the next time you ask for it.
It's like an infinite random database! 🎲