From c7e7041b14dd26caceb986349caf32a4905e5a85 Mon Sep 17 00:00:00 2001 From: Jorge Bodega Date: Tue, 2 Aug 2022 18:13:27 +0200 Subject: [PATCH 1/2] feat: add control of created entities That will allow users to control that entities and remove them after each test --- src/factory.ts | 72 ++++++++++++++++++++++-------- src/index.ts | 2 +- src/subfactories/baseSubfactory.ts | 8 ++++ test/factory.test.ts | 45 +++++++++++++++++++ 4 files changed, 108 insertions(+), 19 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 55337d8..678f8dc 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -1,4 +1,4 @@ -import type { DataSource, SaveOptions } from 'typeorm' +import type { DataSource, RemoveOptions, SaveOptions } from 'typeorm' import { EagerInstanceAttribute, LazyInstanceAttribute } from './instanceAttributes' import { BaseSubfactory } from './subfactories' import type { Constructable, FactorizedAttrs } from './types' @@ -8,6 +8,8 @@ export abstract class Factory { protected abstract dataSource: DataSource protected abstract attrs(): FactorizedAttrs + private createdEntities: T[] = [] + /** * Make a new entity without persisting it */ @@ -42,8 +44,10 @@ export abstract class Factory { const em = this.dataSource.createEntityManager() const savedEntity = await em.save(entity, saveOptions) + this.createdEntities.push(savedEntity) await this.applyLazyAttributes(savedEntity, attrs, true) + return em.save(savedEntity, saveOptions) } @@ -62,22 +66,48 @@ export abstract class Factory { return list } + public getCreatedEntities() { + return this.createdEntities + } + + /** + * This method deletes all entities that were created by this factory. + * The order of deletion is the reverse of the order of creation. + * @experimental As of version 1.2.0 + */ + public async cleanUp(removeOptions?: RemoveOptions) { + const entityManager = this.dataSource.createEntityManager() + for (const entity of this.createdEntities.reverse()) { + await entityManager.remove(entity, removeOptions) + } + } + + public flushEntities() { + this.createdEntities = [] + } + private async makeEntity(attrs: FactorizedAttrs, shouldPersist: boolean) { const entity = new this.entity() await Promise.all( - Object.entries(attrs).map(async ([key, value]) => { - Object.assign(entity, { [key]: await Factory.resolveValue(value, shouldPersist) }) - }), + Object.entries(attrs) + .filter(([, value]) => { + return !(value instanceof EagerInstanceAttribute || value instanceof LazyInstanceAttribute) + }) + .map(async ([key, value]) => { + Object.assign(entity, { [key]: await this.resolveValue(value, shouldPersist) }) + }), ) await Promise.all( - Object.entries(attrs).map(async ([key, value]) => { - if (value instanceof EagerInstanceAttribute) { - const newAttrib = value.resolve(entity) - Object.assign(entity, { [key]: await Factory.resolveValue(newAttrib, shouldPersist) }) - } - }), + Object.entries(attrs) + .filter(([, value]) => value instanceof EagerInstanceAttribute) + .map(async ([key, value]) => { + if (value instanceof EagerInstanceAttribute) { + const newAttrib = value.resolve(entity) + Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist) }) + } + }), ) return entity @@ -85,18 +115,24 @@ export abstract class Factory { private async applyLazyAttributes(entity: T, attrs: FactorizedAttrs, shouldPersist: boolean) { await Promise.all( - Object.entries(attrs).map(async ([key, value]) => { - if (value instanceof LazyInstanceAttribute) { - const newAttrib = value.resolve(entity) - Object.assign(entity, { [key]: await Factory.resolveValue(newAttrib, shouldPersist) }) - } - }), + Object.entries(attrs) + .filter(([, value]) => value instanceof LazyInstanceAttribute) + .map(async ([key, value]) => { + if (value instanceof LazyInstanceAttribute) { + const newAttrib = value.resolve(entity) + Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist) }) + } + }), ) } - private static resolveValue(value: unknown, shouldPersist: boolean) { + private async resolveValue(value: unknown, shouldPersist: boolean) { if (value instanceof BaseSubfactory) { - return shouldPersist ? value.create() : value.make() + if (!shouldPersist) return value.make() + + const [entity, createdEntities] = await value.createAndFlush() + this.createdEntities.push(...createdEntities) + return entity } else if (value instanceof Function) { return value() } else { diff --git a/src/index.ts b/src/index.ts index 9bef88d..96bc2d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ export * from './factory' export * from './instanceAttributes' -export * from './subfactories' +export { CollectionSubfactory, SingleSubfactory } from './subfactories' export * from './types' diff --git a/src/subfactories/baseSubfactory.ts b/src/subfactories/baseSubfactory.ts index bc97e1b..99331bc 100644 --- a/src/subfactories/baseSubfactory.ts +++ b/src/subfactories/baseSubfactory.ts @@ -8,6 +8,14 @@ export abstract class BaseSubfactory { this.factoryInstance = new factory() } + public async createAndFlush() { + const entity = await this.create() + const result = [entity, this.factoryInstance.getCreatedEntities()] + this.factoryInstance.flushEntities() + + return result + } + abstract create(): Promise | Promise abstract make(): Promise | Promise } diff --git a/test/factory.test.ts b/test/factory.test.ts index a28c2a7..2f0ebcc 100644 --- a/test/factory.test.ts +++ b/test/factory.test.ts @@ -90,6 +90,12 @@ describe(Factory, () => { expect(userMaked1).not.toStrictEqual(userMaked2) }) + + test('Should not register maked entity', async () => { + await factory.make() + + expect(factory.getCreatedEntities()).toHaveLength(0) + }) }) describe(PetFactory, () => { @@ -134,6 +140,11 @@ describe(Factory, () => { describe(UserFactory, () => { const factory = new UserFactory() + beforeEach(() => { + factory.cleanUp() + factory.flushEntities() + }) + test('Should create a new entity', async () => { const userCreated = await factory.create() @@ -215,6 +226,40 @@ describe(Factory, () => { expect(userCreated1).not.toStrictEqual(userCreated2) }) + + test('Should register created entity', async () => { + await factory.create() + + expect(factory.getCreatedEntities()).toHaveLength(1) + }) + + test('Should register created entity with subfactory', async () => { + await factory.create({ + pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), + }) + + expect(factory.getCreatedEntities()).toHaveLength(2) + }) + + test('Should remove created entity with function', async () => { + await factory.create({ + pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), + }) + + const em = dataSource.createEntityManager() + + let totalUsers = await em.count(User) + expect(totalUsers).toBe(1) + let totalPets = await em.count(Pet) + expect(totalPets).toBe(1) + + await factory.cleanUp() + + totalUsers = await em.count(User) + expect(totalUsers).toBe(0) + totalPets = await em.count(Pet) + expect(totalPets).toBe(0) + }) }) describe(PetFactory, () => { From dacc97163efa0fd62f99de6f40f3cf9dc19661a0 Mon Sep 17 00:00:00 2001 From: Jorge Bodega Date: Tue, 2 Aug 2022 22:33:18 +0200 Subject: [PATCH 2/2] feat: propagate flag control to subfactories --- src/factory.ts | 82 +++++++++++++++++++----- src/subfactories/baseSubfactory.ts | 6 +- src/subfactories/collectionSubfactory.ts | 4 +- src/subfactories/singleSubfactory.ts | 4 +- test/factory.test.ts | 66 ++++++++++++++----- 5 files changed, 123 insertions(+), 39 deletions(-) diff --git a/src/factory.ts b/src/factory.ts index 678f8dc..0de6f65 100644 --- a/src/factory.ts +++ b/src/factory.ts @@ -16,8 +16,8 @@ export abstract class Factory { async make(overrideParams: Partial> = {}): Promise { const attrs = { ...this.attrs(), ...overrideParams } - const entity = await this.makeEntity(attrs, false) - await this.applyLazyAttributes(entity, attrs, false) + const entity = await this.makeEntity(attrs, false, false) + await this.applyLazyAttributes(entity, attrs, false, false) return entity } @@ -33,35 +33,80 @@ export abstract class Factory { return list } + public async create(overrideParams?: Partial>): Promise + public async create(overrideParams?: Partial>, shouldRegister?: boolean): Promise + public async create(overrideParams?: Partial>, saveOptions?: SaveOptions): Promise + public async create( + overrideParams?: Partial>, + saveOptions?: SaveOptions, + shouldRegister?: boolean, + ): Promise + /** * Create a new entity and persist it */ - async create(overrideParams: Partial> = {}, saveOptions?: SaveOptions): Promise { + public async create( + overrideParams?: Partial>, + saveOptionsOrShouldRegister?: SaveOptions | boolean, + shouldRegister?: boolean, + ): Promise { + const saveOptions = typeof saveOptionsOrShouldRegister === 'object' ? saveOptionsOrShouldRegister : undefined + const shouldRegisterComputed = + typeof saveOptionsOrShouldRegister === 'boolean' ? saveOptionsOrShouldRegister : shouldRegister || false + const attrs = { ...this.attrs(), ...overrideParams } const preloadedAttrs = Object.entries(attrs).filter(([, value]) => !(value instanceof LazyInstanceAttribute)) - const entity = await this.makeEntity(Object.fromEntries(preloadedAttrs) as FactorizedAttrs, true) + const entity = await this.makeEntity( + Object.fromEntries(preloadedAttrs) as FactorizedAttrs, + true, + shouldRegisterComputed, + ) const em = this.dataSource.createEntityManager() const savedEntity = await em.save(entity, saveOptions) - this.createdEntities.push(savedEntity) + if (shouldRegisterComputed) this.createdEntities.push(savedEntity) - await this.applyLazyAttributes(savedEntity, attrs, true) + await this.applyLazyAttributes(savedEntity, attrs, true, shouldRegisterComputed) return em.save(savedEntity, saveOptions) } + public async createMany(amount: number): Promise + public async createMany(amount: number, overrideParams?: Partial>): Promise + public async createMany( + amount: number, + overrideParams?: Partial>, + shouldRegister?: boolean, + ): Promise + public async createMany( + amount: number, + overrideParams?: Partial>, + saveOptions?: SaveOptions, + ): Promise + public async createMany( + amount: number, + overrideParams?: Partial>, + saveOptions?: SaveOptions, + shouldRegister?: boolean, + ): Promise + /** * Create many new entities and persist them */ async createMany( amount: number, overrideParams: Partial> = {}, - saveOptions?: SaveOptions, + saveOptionsOrShouldRegister?: SaveOptions | boolean, + shouldRegister?: boolean, ): Promise { + const saveOptions = typeof saveOptionsOrShouldRegister === 'object' ? saveOptionsOrShouldRegister : undefined + const shouldRegisterComputed = + typeof saveOptionsOrShouldRegister === 'boolean' ? saveOptionsOrShouldRegister : shouldRegister || false + const list = [] for (let index = 0; index < amount; index++) { - list[index] = await this.create(overrideParams, saveOptions) + list[index] = await this.create(overrideParams, saveOptions, shouldRegisterComputed) } return list } @@ -86,7 +131,7 @@ export abstract class Factory { this.createdEntities = [] } - private async makeEntity(attrs: FactorizedAttrs, shouldPersist: boolean) { + private async makeEntity(attrs: FactorizedAttrs, shouldPersist: boolean, shouldRegister: boolean) { const entity = new this.entity() await Promise.all( @@ -95,7 +140,7 @@ export abstract class Factory { return !(value instanceof EagerInstanceAttribute || value instanceof LazyInstanceAttribute) }) .map(async ([key, value]) => { - Object.assign(entity, { [key]: await this.resolveValue(value, shouldPersist) }) + Object.assign(entity, { [key]: await this.resolveValue(value, shouldPersist, shouldRegister) }) }), ) @@ -105,7 +150,7 @@ export abstract class Factory { .map(async ([key, value]) => { if (value instanceof EagerInstanceAttribute) { const newAttrib = value.resolve(entity) - Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist) }) + Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist, shouldRegister) }) } }), ) @@ -113,25 +158,30 @@ export abstract class Factory { return entity } - private async applyLazyAttributes(entity: T, attrs: FactorizedAttrs, shouldPersist: boolean) { + private async applyLazyAttributes( + entity: T, + attrs: FactorizedAttrs, + shouldPersist: boolean, + shouldRegister: boolean, + ) { await Promise.all( Object.entries(attrs) .filter(([, value]) => value instanceof LazyInstanceAttribute) .map(async ([key, value]) => { if (value instanceof LazyInstanceAttribute) { const newAttrib = value.resolve(entity) - Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist) }) + Object.assign(entity, { [key]: await this.resolveValue(newAttrib, shouldPersist, shouldRegister) }) } }), ) } - private async resolveValue(value: unknown, shouldPersist: boolean) { + private async resolveValue(value: unknown, shouldPersist: boolean, shouldRegister: boolean) { if (value instanceof BaseSubfactory) { if (!shouldPersist) return value.make() - const [entity, createdEntities] = await value.createAndFlush() - this.createdEntities.push(...createdEntities) + const [entity, createdEntities] = await value.createAndFlush(shouldRegister) + if (shouldRegister) this.createdEntities.push(...createdEntities) return entity } else if (value instanceof Function) { return value() diff --git a/src/subfactories/baseSubfactory.ts b/src/subfactories/baseSubfactory.ts index 99331bc..d2ca275 100644 --- a/src/subfactories/baseSubfactory.ts +++ b/src/subfactories/baseSubfactory.ts @@ -8,14 +8,14 @@ export abstract class BaseSubfactory { this.factoryInstance = new factory() } - public async createAndFlush() { - const entity = await this.create() + public async createAndFlush(shouldRegister?: boolean) { + const entity = await this.create(shouldRegister) const result = [entity, this.factoryInstance.getCreatedEntities()] this.factoryInstance.flushEntities() return result } - abstract create(): Promise | Promise + abstract create(shouldRegister?: boolean): Promise | Promise abstract make(): Promise | Promise } diff --git a/src/subfactories/collectionSubfactory.ts b/src/subfactories/collectionSubfactory.ts index d24d23e..bd7f7b9 100644 --- a/src/subfactories/collectionSubfactory.ts +++ b/src/subfactories/collectionSubfactory.ts @@ -7,8 +7,8 @@ export class CollectionSubfactory extends BaseSubfactory { super(factory, values) } - create() { - return this.factoryInstance.createMany(this.count, this.values) + create(shouldRegister?: boolean) { + return this.factoryInstance.createMany(this.count, this.values, shouldRegister) } make() { diff --git a/src/subfactories/singleSubfactory.ts b/src/subfactories/singleSubfactory.ts index 10b2a38..23b8c70 100644 --- a/src/subfactories/singleSubfactory.ts +++ b/src/subfactories/singleSubfactory.ts @@ -7,8 +7,8 @@ export class SingleSubfactory extends BaseSubfactory { super(factory, values) } - create() { - return this.factoryInstance.create(this.values) + create(shouldRegister?: boolean) { + return this.factoryInstance.create(this.values, shouldRegister) } make() { diff --git a/test/factory.test.ts b/test/factory.test.ts index 2f0ebcc..8eb9a67 100644 --- a/test/factory.test.ts +++ b/test/factory.test.ts @@ -227,38 +227,61 @@ describe(Factory, () => { expect(userCreated1).not.toStrictEqual(userCreated2) }) - test('Should register created entity', async () => { + test('Should not fail if save options are provided', async () => { + const userCreated = await factory.create( + { + name: 'john', + }, + { + reload: true, + }, + ) + + expect(userCreated.name).toBe('john') + }) + + test('Should not register created entity', async () => { await factory.create() + expect(factory.getCreatedEntities()).toHaveLength(0) + }) + + test('Should register created entity', async () => { + await factory.create({}, true) + expect(factory.getCreatedEntities()).toHaveLength(1) }) test('Should register created entity with subfactory', async () => { - await factory.create({ - pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), - }) + await factory.create( + { + pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), + }, + true, + ) expect(factory.getCreatedEntities()).toHaveLength(2) }) - test('Should remove created entity with function', async () => { - await factory.create({ - pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), - }) + test('Should remove created entity with helper function', async () => { + await factory.create( + { + pets: new LazyInstanceAttribute((instance) => new CollectionSubfactory(PetFactory, 1, { owner: instance })), + }, + true, + ) const em = dataSource.createEntityManager() - let totalUsers = await em.count(User) - expect(totalUsers).toBe(1) - let totalPets = await em.count(Pet) - expect(totalPets).toBe(1) + const totalUsers = await em.count(User) + const totalPets = await em.count(Pet) await factory.cleanUp() - totalUsers = await em.count(User) - expect(totalUsers).toBe(0) - totalPets = await em.count(Pet) - expect(totalPets).toBe(0) + const totalUsersAfterCleanUp = await em.count(User) + expect(totalUsersAfterCleanUp).toBe(totalUsers - 1) + const totalPetsAfterCleanUp = await em.count(Pet) + expect(totalPetsAfterCleanUp).toBe(totalPets - 1) }) }) @@ -298,5 +321,16 @@ describe(Factory, () => { expect(entity.id).toBeDefined() }) }) + + test('Should not fail if save options are provided', async () => { + const count = 2 + const factory = new UserFactory() + const entitiesCreated = await factory.createMany(count, {}, { reload: true }) + + expect(entitiesCreated).toHaveLength(count) + entitiesCreated.forEach((entity) => { + expect(entity.id).toBeDefined() + }) + }) }) })