Skip to content

Commit

Permalink
New test data models documentation (#687)
Browse files Browse the repository at this point in the history
* docs: test data models overview document

* docs: include new test data model guidelines docs

* fix(docs): apply first feedback for data models overview doc

* fix(docs): apply first feedback for building data model doc

* docs: apply feedback

* docs: apply feedback

* docs: fix wording

* docs: apply pr feedback
  • Loading branch information
CarlosCortizasCT authored Oct 8, 2024
1 parent 6bdcbe6 commit a6a39db
Show file tree
Hide file tree
Showing 3 changed files with 450 additions and 0 deletions.
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 when writing their own tests in order to mock the data returned by commercetools services.

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 of:

- A Rest builder
- A GraphQL builder
- TypeScript types

A `Builder` is the object you are interacting with as a consumer. It has only two functions:

- `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 an entry point of a data model package looks like:

```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 = {
random: GraphqlModelBuilder,
presets: GeometryPresets.graphqlPresets,
};
```

### Examples

#### Building default random data models

```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

```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

Test data model are implemented using some classes and helpers exposed from the [core](https://github.com/commercetools/test-data/tree/main/core) package.

### 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, these files are required to export two configuration objects (`restFieldsConfig`, `graphqlFieldsConfig`) which describe how to populate the properties of the data model when building the default version.

The configuration objects have two properties:

- `fields` (**required**): Describe how the default values should be populated
- `postBuild` (_optional_): Callback function to run modifications on the built data model

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 case, `name` and `description` depend on the values from `nameAllLocales` and `descriptionAllLocales` but we don't know them in advance, only once the data model has been built. The `postBuild` callback allows to manipulate the built object before it's returned to the consumer.

This function just need to return an object with the updated properties and their values and it will be merged with the originally built one.

### Builders

The `builders` is another main file in which we actually create the objects which implement the fluent API that allows generating data models objects.

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,
});
```

We're exporting one builder for each representation (REST and GraphQL) of the data models for each of which we use the respective `fields-config` file detailed above.

## 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:

```ts
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

0 comments on commit a6a39db

Please sign in to comment.