@mixtape/core
v1.3.0
Published
Supercharged fixture library for organizing and generating test data
Downloads
142
Maintainers
Readme
Mixtape
A fixture library, written in TypeScript, for organizing and generating random test data for JavaScript/Node.js applications. Using this library should make it easy to arrange and maintain tests.
This library is heavily inspired by the C# library AutoFixture.
Table of Contents
Installation 💾
Install the library with npm
npm install --save-dev @mixtape/core
or with yarn
yarn add --dev @mixtape/core
Getting Started 🚀
Templates and Injectors
The fastest way to get started with Mixtape is to create an injector (with a Fixture
constructor function), use the injector to provide the fixture in the tests and make a template as a blueprint for generating test data. Here is an example:
const { Fixture, createInjector } = require('@mixtape/core');
const withFixture = createInjector(() => new Fixture());
test('test template with Mixtape', withFixture(fixture => {
const heroTemplate = {
name: 'string',
powers: ['string'],
age: 'number',
hasSecretIdentity: 'boolean',
origin: {
planet: 'string',
parents: 'undefined'
}
}
const randomHero = fixture.from(heroTemplate).create();
expect(typeof randomHero.name).toBe('string');
expect(randomHero.powers instanceof Array).toBeTruthy();
expect(typeof randomHero.age).toBe('number');
expect(typeof randomHero.hasSecretIdentity).toBe('boolean');
expect(typeof randomHero.origin).toBe('object');
expect(typeof randomHero.origin.planet).toBe('string');
expect(randomHero.origin.parents).toBeUndefined();
}));
ℹ️ For injector functions: Additional arguments from the testing framework is passed after the fixture object, e.g.
withFixture((fixture, ...args) => {})
.
Creating Builders
To make things easier to maintain and to keep the tests DRY, builders can be used instead of templates. A builder can be created and added to the extensions
property of the Fixture
object like this:
const { Fixture, Builder } = require('@mixtape/core');
class SuperHeroBuilder extends Builder {
constructor() {
// Name of the type that the builder can create
super('SuperHero');
}
/**
* The `context` (a subset of Fixture methods) can be use to create other types inside the builder
* Using the `context` to generate data is needed for methods like ´freeze()´ to work
*/
build(context) {
return {
name: context.create('string'),
powers: context.createMany('string', 3),
age: context.create('number'),
hasSecretIdentity: context.create('boolean')
}
}
}
const fixture = new Fixture();
fixture.extensions.add(new SuperHeroBuilder());
const randomHero = fixture.create('SuperHero');
/**
* Value of randomHero
* {
name: 'a716b96b-3ede-4cca-8f5a-07629a3d9e2b',
powers: [
'f12183b5-08d9-4948-8259-46b6342a630d',
'b271e647-2eae-4629-826f-22987df5d349',
'e59af318-56a3-4b26-a8ff-6f0dbd1704d1'
],
age: 117,
hasSecretIdentity: true
}
*/
ℹ️ Instead of using strings to denote primitive types an object (
PrimitiveType
) is also available with these types. Then creating primitive types looks likes thisfixture.create(PrimitiveType.string)
.
In ES5 a similar builder looks like this:
var SuperHeroBuilder = { type: 'SuperHero', build: function(context) { return { name: context.create('string'), powers: context.createMany('string', 3), age: context.create('number'), hasSecretIdentity: context.create('boolean') } } }
When a builder has been added to a Fixture
then it can be used by other builders (or in templates). Maybe - building on the hero example - the age of a superhero should meet a certain criteria, i.e. value must be between 18 and 99.
const { Fixture, Builder, NumberGenerator } = require('@mixtape/core');
class SuperHeroBuilder extends Builder {
constructor() {
super('SuperHero');
}
build(context) {
return {
name: context.create('string'),
powers: context.createMany('string', 3),
age: context.create('HeroAge'),
hasSecretIdentity: context.create('boolean')
}
}
}
class SuperHeroAgeBuilder extends Builder {
constructor() {
super('HeroAge');
// Use a random number generator to control the age
this.generator = new NumberGenerator(18, 99);
}
build() {
return this.generator.generate();
}
}
fixture.extensions.add(new SuperHeroBuilder());
fixture.extensions.add(new SuperHeroAgeBuilder());
const randomHero = fixture.create('SuperHero');
/**
* Value of randomHero
* {
name: '66cbacff-2e85-4403-a986-2337a28520ca',
powers: [
'4e1bab7b-2548-4fbc-9f80-e9c4f747317d',
'687fa7f2-250e-40fa-a561-c2ad52d91c82',
'3fb8e2e9-d715-4b9e-a6f2-3f6986ac0229'
],
age: 88,
hasSecretIdentity: true
}
*/
This ensures that all generated heroes will have an age between 18 and 99.
Bundle Builders Using Extensions
Now that we have two builders it would be a good idea to bundle them in an Extension
like this
const superHeroExtension = new Extension();
superHeroExtension.add(new SuperHeroBuilder());
superHeroExtension.add(new SuperHeroAgeBuilder());
// Now add it to a fixture like this
const fixture = new Fixture();
fixture.extend(superHeroExtension);
// or use it in the creation of an injector like this
const withHeroFixture = createInjector(() => new Fixture().extend(superHeroExtension));
This makes it easy to group related builders and easily add them to Fixtures as needed.
Freeze Properties
In some test cases a number of objects need to have the same value for a specific property; this can be achieved by calling freeze()
on the Fixture
. In this case a random sized array of heroes with same age can be created like this:
fixture.freeze('HeroAge');
const heroesWithSameAge = fixture.createMany('SuperHero');
/**
* Value of heroesWithSameAge
* [
{
name: '7869ced3-ff7d-4e0b-9bfb-b29d5b36d980',
powers: [
'be8f0ec5-1281-436a-9a25-3fc78086dc91',
'678e5e93-4bae-4b19-b75a-0b71bf43656a',
'8f1d9dbe-e91a-4c2e-b5f5-7d4ed270546e'
],
age: 37,
hasSecretIdentity: false
},
{
name: '3ed5d9e5-25c3-413c-8032-60958a731414',
powers: [
'77860d28-d1b2-428e-8b1a-ded618d8314d',
'68429cda-f63e-48a3-88c5-f7b7fda6fdd7',
'102e20df-07ef-4699-87c4-c558e5a14dc4'
],
age: 37,
hasSecretIdentity: true
},
{
name: 'f8767886-e72e-42ac-9b7d-1a4a3ccbe87a',
powers: [
'232e377e-c6d5-461f-8f52-6b3248719839',
'b1e45387-50de-4424-a00c-808fddb259f0',
'e56ca196-7515-4efd-83cd-0b5812a4e859'
],
age: 37,
hasSecretIdentity: false
}
.
.
.
]
*/
ℹ️ If the property - in our case
age
- needs to have a specific value then the methoduse()
can be utilized instead. Also, the methodreset()
can be used to clear all frozen values and values defined viause()
.
Custom Objects
In other test cases a custom build object is needed and for this build()
can be called on the Fixture
.
const customHero = fixture
.build('SuperHero')
.with('name', () => 'Wolverine')
.with('powers', p => ['healing', 'endurance', ...p])
.without('hasSecretIdentity')
.create();
/**
* Value of customHero
* {
name: 'Wolverine',
powers: [
'healing',
'endurance',
'af537167-863c-42ca-8181-31f1fcb25115',
'250f05b4-b0ea-45c4-b0d4-2b6efbe26172',
'40a746c0-b361-4428-a894-86edefa61e17'
],
age: 59
}
*/
ℹ️ Accessing a nested property is not possible using
with()
/without()
. A way around this is to usedo()
instead. For instancefixture.build('type').do(t => t.nestedObject.value = 'newValue')
.
Documentation 📄
More details about this library can be found in the documentation here.