typeorm-fixture
v0.3.6
Published
Painless TypeORM fixtures
Downloads
33
Maintainers
Readme
typeorm-fixture
typeorm-fixture provides an easy and consistant way to load test fixtures into a database with TypeORM.
This library provides three constructs:
- Factories creates entities with random or partially prepared data.
- Static fixtures load entities and data with fixed data when a test suite starts.
- Dynamic fixtures load entities and data during a test suite, and can be parameterized to fit your needs.
Along with these, typeorm-fixture has many features that help us spend more time on writing actual tests.
Installation
With npm
npm install --dev typeorm-fixture
or with yarn
yarn add -D typeorm-fixture
TypeORM is a peer dependency.
experimentalDecorators
must be set to true in tsconfig.json
, although this already should have been set if TypeORM is installed.
Features
For demonstration, let's load some test data.
// src/models.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column()
name!: string;
@Column()
point!: number;
}
@Entity()
export class Article {
@PrimaryGeneratedColumn()
id!: number;
@Column()
content!: string;
@CreateDateColumn()
createdAt!: Date;
@ManyToOne(() => User, { nullable: false }) @JoinColumn()
author!: User;
@Column({ nullable: false })
authorId!: number;
@Column({ default: 0 })
likeCount!: number;
}
@Entity()
export class ArticleLike {
@PrimaryGeneratedColumn() id!: number;
@ManyToOne(() => User, { nullable: false }) @JoinColumn()
user!: User;
@Column({ nullable: false })
userId!: number;
@ManyToOne(() => Article, { nullable: false }) @JoinColumn()
article!: Article;
@Column({ nullable: false })
articleId!: number;
}
Factories
Factories create entities with random or partially prepared data. Only the createRandom()
abstract method need to be implemented, and the BaseFactory
takes care of everything else. After defining createRandom()
, factories can be used in fixtures or in test cases.
Factories should not have access to the database, as its role is only to create entity instances. All factories should be created by extending BaseFactory<EntityType>
, and with the @Factory(EntityType)
decorator.
// src/test/factory/User.ts
import faker from 'faker';
@Factory(User)
export default class UserFactory extends BaseFactory<User> {
protected createRandom(): User {
return {
name: faker.name.firstName(),
point: faker.datatype.number({min: 0, max: 1000}),
};
}
}
Even though createRandom
returns a plain object and not an User
instance, BaseFactory
will convert the object and return an User
instance when random
is used.
We need another factory for the Article entity.
// src/test/factory/Article.ts
import faker from 'faker';
@Factory(Article)
export default class ArticleFactory extends BaseFactory<Article> {
protected createRandom(): Article {
return {
const article = new Article();
article.content = faker.sentence();
return article;
}
}
}
Static Fixtures
Factories alone cannot persist databases. Static fixtures provide a pattern to load fixed entities and data when a test suite starts.
We can start by creating and inserting 10 random users.
// src/test/fixture/User.ts
@StaticFixture()
export default class UserFixture extends BaseStaticFixture<User[]> {
public async install(manager: EntityManager): Promise<User[]> {
let users = this.factoryOf(User).randomMany(10);
users = await manager.getRepository(User).save(user);
return users;
}
}
Some things to notice:
EntityManager
is the entity manager provided by TypeORM.this.factoryOf(User)
returns a factory instance forUser
. This would be the UserFactory we have defined earlier..randomMany(10)
is a handy method defined inBaseFactory
for creating multiple random entites at once.- The users returned by
install
will be cached and be available for other fixtures that may depend onUserFixture
. The type of the cached value can be freely chosen, such asRecord<string, User>
for mapping on a key or simplyvoid
when caching isn't needed.
Now that we have setup users, we can create articles.
// src/test/fixture/Article.ts
@StaticFixture({ dependencies: [UserFixture] })
export default class ArticleFixture extends BaseStaticFixture<Article[]> {
public async install(manager: EntityManager): Promise<Article[]> {
const users = this.fixtureResultOf(UserFixture);
let articles = users.map((user) => this.factoryOf(Article).partial({author: user}));
articles = await manager.getRepository(Article).save(articles);
return articles;
}
}
In ArticleFixture, dependencies are provided in @StaticFixture
. This ensures that UserFixture
will be installed before ArticleFixture
. By adding UserFixture into the dependency, the cached result provided by UserFixture can safely be loaded by calling this.fixtureResultOf(UserFixture)
.
We can also see that this.factoryOf(Article).partial
is called. partial
is also a method defined in BaseFactory
, allowing us to override some properties over a random entity made in createRandom
.
Dynamic Fixtures
Because static fixtures are intended to be used only before starting any test cases, they have little flexibility. They are only used once, so they don't accept parameters. We can't obtain instances of static fixtures as they aren't meant to be used multiple times. However, there always is a time when you need to add additional test data during test suites. Factories can be used during testing, but they are tied to a single entity and cannot do database operations.
Dynamic fixtures provide a reusable and consistant with static fixtures to create data, while having the flexibility to accept parameters. For example, creating a group of entities that have one-to-one relationships with each other is a good fit for using dynamic fixtures.
In our example, we can create a dynamic fixture for creating ArticleLike entities.
/// src/test/fixture/ArticleLike.ts
interface ArticleLikeOptions {
article: Article;
user: User;
}
@DynamicFixture({ isolationLevel: 'SERIALIZABLE' })
export default class ArticleLikeFixture extends BaseDynamicFixture<ArticleLike, ArticleLikeOptions> {
public async install(manager: EntityManager, options: ArticleLikeOptions): Promise<ArticleLike> {
const articleLike = new ArticleLike();
articleLike.article = options.article;
articleLike.user = options.user;
await manager.getRepository(ArticleLike).save(articleLike);
article.likeCount += 1;
await manager.getRepository(Article).save(article);
return articleLike;
}
}
Some things to keep in mind are:
- Dynamic fixtures and static fixtures are both fixtures. They can both be dependent on each other, and obtain instances/results of each other.
- If the
isolationLevel
field is provided, the whole fixture is run within a single transaction.isolationLevel
can be one of the values in TypeORM'sIsolationLevel
type, ordefault
(using the default database isolation level). This can also used in static fixtures.
Bringing everything together
We haven't described how we can use the classes we've created. Everything we made can be imported and used though the FixtureContainer
.
A FixtureContainer should be kept somewhere you can always access during tests. For instance in jest, this could be a variable inside the outermost describe
block.
describe('My database', () => {
let fixtureContainer;
beforeAll(async () => {
// After establishing a database connection..
fixtureContainer = new FixtureContainer({
filePatterns: ["src/test/**/*.ts"],
});
await fixtureContainer.loadFiles();
await fixtureContainer.installFixtures();
});
});
.loadFiles()
reads the files given in the glob pattern, and .installFixtures()
executes (installs) the static fixtures. Note that even if you don't have any static fixtures, you must run .loadFiles()
to use factories and dynamic fixtures.
FixtureContainers can also accept direct class constructors, or both.
fixtureContainer = new FixtureContainer({
filePatterns: ["src/test/**/*.ts"],
fixtures: [SomeOtherFixture, SomeDynamicFixture],
factories: [MyFactory],
});
After initializing the container, we can use the loaded classes/results.
.factoryOf(EntityType)
returns a factory instance forEntityType
..fixtureResultOf(StaticFixtureType)
returns the cached result of theStaticFixtureType
static fixture..dynamicFixtureOf(DynamicFixtureType)
returns a dynamic fixture instance forDynamicFixtureType
.
Reusing a single fixture container
If reusing (reinstalling) static fixtures is necessary, we can do so by clearing the container cache and reinstalling fixtures.
describe('My database', () => {
let fixtureContainer;
beforeAll(async () => {
// After establishing a database connection..
fixtureContainer = new FixtureContainer({
filePatterns: ["src/test/**/*.ts"],
});
await fixtureContainer.loadFiles();
await fixtureContainer.installFixtures();
});
afterEach(async () => {
await fixtureContainer.clearFixtureResult();
await fixtureContainer.installFixtures();
});
});
Advanced Features
Saving directly from a factory
Factories are often used for creating and directly saving entities. As a result, we often write a pattern like this:
// manager is a TypeORM EntityManager instance
const testEntity = await manager.getRepository(MyEntity).save(fixtureContainer.factoryOf(MyEntity).random());
and the line goes longer if we use partial
, partialMany
, etc.
This pattern can be shorter by providing the EntityManager to the factory. The factory will create object(s) as the same way as usual, then save and return the object(s).
const testEntity = await fixtureContainer.factoryOf(MyEntity).saving(manager).random();
This is equivalent to the example using .getRepository()
.
License
MIT