Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New test data models documentation #687

Merged
merged 8 commits into from
Oct 8, 2024
Merged
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ To know more about how to work and build data models, [check out the documentati

Once it's done, you can run `pnpm test` to test your changes.

## Building a new test data model

Please refer to [this documentation](docs/guidelines/creating-new-model.md) to get detailed information.

## Adding changesets

commercetools test-data uses [changesets](https://github.com/atlassian/changesets) to do versioning and creating changelogs.
Expand Down
250 changes: 250 additions & 0 deletions docs/contributing/test-data-models-overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
# Test data models overview

A test data model is set of helper objects commercetools API consumers can use during writing their own tests in order to mock the data returned by our services.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

These helpers are flexible enough to represent different types of responses from different APIs and with different values.

## Public API

Each test data model has the same API which consists on:
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

- A Rest builder
- A Graphql builder
tdeekens marked this conversation as resolved.
Show resolved Hide resolved
- Typescript types
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

A `Builder` is the object you would be interacting with as a consumer and it has only two functions:
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

- `random`: exposes a fluent API which allows to build a customized version of the data model
- `presets`: predefined versions of the data model

This is how it looks like the entry point of a data model package:
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

```ts
import { RestModelBuilder, GraphqlModelBuilder } from './builders';
import * as GeometryPresets from './presets';

export * from './types';

export const GeometryRest = {
random: RestModelBuilder,
presets: GeometryPresets.restPresets,
};

export const GeometryGraphql = {
CarlosCortizasCT marked this conversation as resolved.
Show resolved Hide resolved
random: GraphqlModelBuilder,
presets: GeometryPresets.graphqlPresets,
};
```

### Examples

#### Building default random data models
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

```ts
import {
RestModelBuilder,
GraphqlModelBuilder,
type TGeometryRest,
type TGeometryGraphql,
} from '@commercetools-test-data/geometry';

const restGeometry: TGeometryRest = RestModelBuilder.random().build();
const graphqlGeometry: TGeometryGraphql = GraphqlModelBuilder.random().build();
```

#### Building customized data models

```ts
import {
RestModelBuilder,
GraphqlModelBuilder,
type TGeometryRest,
type TGeometryGraphql,
} from '@commercetools-test-data/geometry';

const restGeometry: TGeometryRest = RestModelBuilder.random()
.coordinates([12.45, 66.33])
.build();
const graphqlGeometry: TGeometryGraphql = GraphqlModelBuilder.random()
.coordinates([12.45, 66.33])
.build();
```

#### Building predefined version data models
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

```ts
import {
RestModelBuilder,
GraphqlModelBuilder,
type TGeometryRest,
type TGeometryGraphql,
} from '@commercetools-test-data/geometry';

const restGeometry: TGeometryRest = RestModelBuilder.presets.munich().build();
const graphqlGeometry: TGeometryGraphql = GraphqlModelBuilder.presets
.munich()
.build();
```

## Internal structure

The way a data model is implemented is by using some classes and helpers exposed from the [core](https://github.com/commercetools/test-data/tree/main/core) package.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

### Fields config

The core file is named `fields-config` and its responsibility is to describe how the data model properties should be pre-populated and also resolve dependencies among them (if any).

Here's an example of a simple one:

```ts
import { fake, type TModelFieldsConfig } from '@commercetools-test-data/core';
import type { TGeometryGraphql, TGeometryRest } from './types';

const commonFieldsConfig = {
type: fake(() => 'Point'),
coordinates: fake((f) => [f.location.longitude(), f.location.latitude()]),
};

export const restFieldsConfig: TModelFieldsConfig<TGeometryRest> = {
fields: {
...commonFieldsConfig,
},
};

export const graphqlFieldsConfig: TModelFieldsConfig<TGeometryGraphql> = {
fields: {
...commonFieldsConfig,
__typename: 'Geometry',
},
};
```

As you can see, this files is required to export two configuration objects (`restFieldsConfig`, `graphqlFieldsConfig`) which describe how to populate the properties of the data model when building the default version.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

The configuration objects has two properties:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The configuration objects has two properties:
The configuration object has two properties:

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used plural on purpose because we have two configuration objects, one for rest and another for graphql, and I'm trying to refer to them.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The configuration objects has two properties:
The configuration objects have two properties:

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is closer to your intended meaning here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Tyler 🙇

Updated here: 7b0f7aa


- `fields` (**required**): Describe how the default values should be populated
- `postBuild` (_optional_): Callback function to run modifications in the built data model
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

Here's an example where we use `postBuild` option to populate some values which depend on others to be built first:

```ts
export const graphqlFieldsConfig: TModelFieldsConfig<TChannelGraphql> = {
fields: {
...commonFieldsConfig,
__typename: 'Channel',
nameAllLocales: null,
descriptionAllLocales: null,
},
postBuild: (model) => {
const name = model.nameAllLocales
? LocalizedString.resolveGraphqlDefaultLocaleValue(model.nameAllLocales)
: undefined;
const description = model.descriptionAllLocales
? LocalizedString.resolveGraphqlDefaultLocaleValue(
model.descriptionAllLocales
)
: undefined;
return {
name,
description,
};
},
};
```

In this example, `name` and `description` are values that are calculated based on the `nameAllLocales` and `descriptionAllLocales` ones but we need the latter to be built first, so that why we use the `postBuild` function.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

This function just need to return an object with the updated properties values and it will be merged with the originally built one.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

### Builders

The other main file of the data model is the `builders` one where we actually create the objects which implement the fluent API that allows generating data models objects.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

This is how it looks like:

```ts
import { createSpecializedBuilder } from '@commercetools-test-data/core';
import { restFieldsConfig, graphqlFieldsConfig } from './fields-config';
import type {
TCreateGeometryBuilder,
TGeometryGraphql,
TGeometryRest,
} from './types';

export const RestModelBuilder: TCreateGeometryBuilder<TGeometryRest> = () =>
createSpecializedBuilder({
name: 'GeometryRestBuilder',
type: 'rest',
modelFieldsConfig: restFieldsConfig,
});

export const GraphqlModelBuilder: TCreateGeometryBuilder<
TGeometryGraphql
> = () =>
createSpecializedBuilder({
name: 'GeometryGraphqlBuilder',
type: 'graphql',
modelFieldsConfig: graphqlFieldsConfig,
});
```

Basically we're exporting one builder for each representation (REST and GRAPHQL) of the data models where we use the `fields-config` file detailed above.
tdeekens marked this conversation as resolved.
Show resolved Hide resolved

## Testing

It's very important we implement relevant tests for our data models builders so we can make sure consumers will be creating the right data out of our exported assets.

The bare minimum tests to implement are those which validate the generated default models contain the expected base data.

Here's an example:

```
tdeekens marked this conversation as resolved.
Show resolved Hide resolved
import { GeometryRest, GeometryGraphql } from './index';

describe('Geometry Builder', () => {
it('should build properties for the REST representation', () => {
const restModel = GeometryRest.random().build();

expect(restModel).toEqual(
expect.objectContaining({
type: 'Point',
coordinates: expect.arrayContaining([
expect.any(Number),
expect.any(Number),
]),
})
);
});
it('should build properties for the GraphQL representation', () => {
const graphqlModel = GeometryGraphql.random().build();

expect(graphqlModel).toEqual(
expect.objectContaining({
type: 'Point',
coordinates: expect.arrayContaining([
expect.any(Number),
expect.any(Number),
]),
__typename: 'Geometry',
})
);
});
});
```

It's also recommended to test a customized version of the data models as well:

```ts
it('should build a customized version of the GraphQL data model', () => {
const graphqlModel = GeometryGraphql.random().coordinates([10, 20]).build();

expect(graphqlModel).toEqual(
expect.objectContaining({
type: 'Point',
coordinates: expect.arrayContaining([10, 20]),
__typename: 'Geometry',
})
);
});
```
Loading