@mixtape/core
TypeScript icon, indicating that this package has built-in type declarations

1.3.0 • Public • Published

Mixtape

Build Status Coverage Status npm npm David David license

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 this fixture.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 method use() can be utilized instead. Also, the method reset() can be used to clear all frozen values and values defined via use().

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 use do() instead. For instance fixture.build('type').do(t => t.nestedObject.value = 'newValue').

Documentation 📄

More details about this library can be found in the documentation here.

Package Sidebar

Install

npm i @mixtape/core

Weekly Downloads

1

Version

1.3.0

License

MIT

Unpacked Size

215 kB

Total Files

71

Last publish

Collaborators

  • legaard