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

feat: generic factory #1200

Merged
merged 1 commit into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions backend/src/libs/test-utils/mocks/factories/board-factory.mock.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import faker from '@faker-js/faker';
import { ColumnFactory } from './column-factory.mock';
import Board from 'src/modules/boards/entities/board.schema';
import { buildTestFactory } from './generic-factory.mock';

const userId = faker.datatype.uuid();

const mockBoardData = (countColumns = 2, countCards = 1, params?: Partial<Board>) => {
const mockBoardData = () => {
return {
_id: faker.datatype.uuid(),
title: faker.lorem.words(),
columns: ColumnFactory.createMany(countColumns, countCards),
columns: ColumnFactory.createMany(3),
isPublic: faker.datatype.boolean(),
maxVotes: String(faker.datatype.number({ min: 0, max: 6 })),
maxVotes: faker.datatype.number({ min: 0, max: 6 }),
maxUsers: 0,
maxTeams: '1',
hideCards: faker.datatype.boolean(),
Expand All @@ -28,13 +29,10 @@ const mockBoardData = (countColumns = 2, countCards = 1, params?: Partial<Board>
createdBy: userId,
addcards: faker.datatype.boolean(),
postAnonymously: faker.datatype.boolean(),
...params
createdAt: faker.datatype.datetime().toISOString()
};
};

export const BoardFactory = {
create: (countColumns = 1, countCards = 1, params?: Partial<Board>) =>
mockBoardData(countColumns, countCards, params),
createMany: (count = 1, countColumns = 2, countCards = 1, params?: Partial<Board>) =>
Array.from({ length: count }).map(() => BoardFactory.create(countColumns, countCards, params))
};
export const BoardFactory = buildTestFactory<Board>(() => {
return mockBoardData();
});
14 changes: 6 additions & 8 deletions backend/src/libs/test-utils/mocks/factories/card-factory.mock.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import faker from '@faker-js/faker';
import Card from 'src/modules/cards/schemas/card.schema';
import { buildTestFactory } from './generic-factory.mock';

const userId = faker.datatype.uuid();
const cardId = faker.datatype.uuid();
Expand All @@ -8,7 +9,7 @@ const commentText = faker.lorem.paragraph(1);
const teamId = faker.datatype.uuid();
const createdAtDate = faker.datatype.datetime();

const mockCardData = (params: Partial<Card>): Card => {
const mockCardData = (): Card => {
return {
_id: cardId,
text: cardText,
Expand Down Expand Up @@ -41,13 +42,10 @@ const mockCardData = (params: Partial<Card>): Card => {
createdByTeam: teamId,
createdAt: createdAtDate
}
],
...params
]
};
};

export const CardFactory = {
create: (params: Partial<Card> = {}): Card => mockCardData(params),
createMany: (amount: number, params: Partial<Card> = {}): Card[] =>
Array.from({ length: amount }).map(() => CardFactory.create(params))
};
export const CardFactory = buildTestFactory<Card>(() => {
return mockCardData();
});
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import faker from '@faker-js/faker';
import Column from 'src/modules/columns/entities/column.schema';
import { CardFactory } from './card-factory.mock';
import { buildTestFactory } from './generic-factory.mock';

const cardText = faker.lorem.words();

const mockColumnData = (params: Partial<Column>, cardsCount = 1): Column => {
const mockColumnData = (cardsCount = 1): Column => {
return {
_id: faker.datatype.uuid(),
title: faker.lorem.words(),
color: '#aaaaaa',
cards: CardFactory.createMany(cardsCount),
cardText: cardText,
isDefaultText: faker.datatype.boolean(),
...params
isDefaultText: faker.datatype.boolean()
};
};

export const ColumnFactory = {
create: (params: Partial<Column> = {}, cardsCount = 1): Column =>
mockColumnData(params, cardsCount),
createMany: (amount: number, cardsCount = 1, params: Partial<Column> = {}): Column[] =>
Array.from({ length: amount }).map(() => ColumnFactory.create(params, cardsCount))
};
export const ColumnFactory = buildTestFactory<Column>(() => {
return mockColumnData();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import range from 'src/libs/utils/range';
import { FixedLengthArray, ObjectOfAny } from 'src/libs/utils/types';

const DEFAULT_CREATE_MANY_COUNT = 10;

export type BuildTestFactoryCountValue = number | 'random';

export type BuildTestFactoryGetterProps<TModel> = (index: number) => Partial<TModel>;

export type BuildTestFactoryArrayProps<TModel, TCount extends number> = FixedLengthArray<
Partial<TModel>,
TCount
>;

type BuildTestFactoryProps<
TModel,
TCount extends BuildTestFactoryCountValue
> = TCount extends number
? BuildTestFactoryGetterProps<TModel> | BuildTestFactoryArrayProps<TModel, TCount>
: BuildTestFactoryGetterProps<TModel>;

const isGetterProps = <TModel>(props: unknown): props is BuildTestFactoryGetterProps<TModel> =>
typeof props === 'function';

const isArrayProps = <TModel>(props: unknown, index: number): props is Array<TModel> =>
Array.isArray(props) && index < props.length;

const getOverrideProps = <TModel, TCount extends number>(
props: BuildTestFactoryProps<TModel, TCount> | undefined,
index: number
): Partial<TModel> => {
let overrideProps: Partial<TModel> = {};

if (isGetterProps<TModel>(props)) {
overrideProps = props(index);
} else if (isArrayProps<TModel>(props, index)) {
overrideProps = props[index];
}

return overrideProps;
};

function getItemCount(count?: BuildTestFactoryCountValue): number {
if (!count) return DEFAULT_CREATE_MANY_COUNT;

if (count === 'random') return Math.floor(Math.random() * 100) + 1;

return count;
}

/**
* Provides functions to create items from a given factory.
* @example
*
* const UserFactory = buildTestFactory<{ name: string, age: number }>((index) => ({
* name: faker.name.firstName(),
* age: faker.datatype.number(1, index === 0 ? 50 : 100),
* }));
* // single item
* UserFactory.create();
* // single item with overriden props
* UserFactory.create({ name: 'John' });
* // multiple items
* UserFactory.createMany(3);
* // multiple items with overriden props on every item
* UserFactory.createMany(3, () => ({ name: 'John' }));
* // multiple items with overriden props per index
* UserFactory.createMany(3, [{ name: 'John' }, { name: 'Jane' }, { name: 'Jack' }]);
*/
export const buildTestFactory = <TModel extends ObjectOfAny>(
factory: (index?: number) => TModel
) => ({
create: (props?: Partial<TModel>): TModel => ({ ...factory(), ...props }),
createMany: <TCount extends BuildTestFactoryCountValue>(
count?: TCount,
props?: BuildTestFactoryProps<TModel, TCount>
): TModel[] =>
range(1, getItemCount(count)).map((_, index) => ({
...factory(index),
...getOverrideProps(props, index)
}))
});
7 changes: 7 additions & 0 deletions backend/src/libs/utils/range.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const range = (startAt: number, size: number): Array<number> => {
const length = size - startAt + 1;

return Array.from({ length }, (_, i) => i + startAt);
};

export default range;
17 changes: 17 additions & 0 deletions backend/src/libs/utils/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export type ObjectOfAny = Record<string, any>;

/**
* Enforces a fixed size hardcoded array.
* @see https://github.com/microsoft/TypeScript/issues/18471#issuecomment-776636707 for the source of the solution.
* @see https://github.com/microsoft/TypeScript/issues/26223#issuecomment-674514787 another approach, that might be useful if the one above shows any limitation.
* @example
* const fixedArray: FixedSizeArray<string, 3> = ['a', 'b', 'c'];
*/
export type FixedLengthArray<T, N extends number> = N extends N
? number extends N
? T[]
: FixedLengthArrayRecursive<T, N, []>
: never;
type FixedLengthArrayRecursive<T, N extends number, R extends unknown[]> = R['length'] extends N
? R
: FixedLengthArrayRecursive<T, N, [T, ...R]>;
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { getTeamService, teamRepository, teamUserRepository } from 'src/modules/
import { updateUserService, userRepository } from 'src/modules/users/users.providers';
import { JwtService } from '@nestjs/jwt';

const fakeBoards = BoardFactory.createMany(2, 3, 2);
const fakeBoards = BoardFactory.createMany(2);

describe('UpdateColumnService', () => {
let columnService: UpdateColumnServiceImpl;
Expand Down Expand Up @@ -176,7 +176,7 @@ describe('UpdateColumnService', () => {

describe('delete cards from column', () => {
it('should return a updated board without cards on the column', async () => {
const fakeBoards = BoardFactory.createMany(2, 3, 2);
const fakeBoards = BoardFactory.createMany(2);
const boardId = fakeBoards[1]._id;
const boardResult = fakeBoards[1];
const columnsResult = fakeBoards[1].columns.map((col) => {
Expand Down Expand Up @@ -241,7 +241,7 @@ describe('UpdateColumnService', () => {
});

it("when given column_id doesn't exist, throw Bad Request Exception", async () => {
const fakeBoards = BoardFactory.createMany(2, 0, 0);
const fakeBoards = BoardFactory.createMany(2);
const boardId = fakeBoards[1]._id;
const columnToDeleteCards = {
id: faker.datatype.uuid(),
Expand Down