Home

Awesome

Feathers Factory

A dead easy way to mock data for testing your Feathers services. Heavily inspired by Meteor Factory

Tests Downloads Version

Installation

npm install --save-dev feathers-factory

Example Usage

Feathers Factory provides an easy way define data generation templates for testing Feathers services. This works really nicely together with a mock data generator like faker.

Define and export a factory for your tests

Just new up the Feathers Factory class, provide your Feathers service, and a "Generator" object which defines what values should be inserted into your service when the factory is called.

// ./tests/Factories.ts
import FeathersApp from '../src/App'
import { Factory } from 'feathers-factory';

export const UserFactory = new Factory(FeathersApp.service('users'), {
    username: () => Faker.internet.userName(),
    membership: 'bronze'
});

Types for the factory are inferred from the Feathers service you provide. It's fairly strict by design, so it may give you some issues depending on how your service is defined. See Global Factories for more info on how to work around this.

Import and use your factory

// ./tests/services/users.test.ts
import { UserFactory } from '../../Factories';
import FeathersApp from '../../../src/App';

describe('Users', () => {
    const service = FeathersApp.service('users');
    
    it('can be removed', async () => {
        const user = await UserFactory.create(); 
        // { _id: "507f191e810c19729de860ea", username: "Damaris8", membership: "bronze" }
    
        await expect(service.get(user._id)).resolves.toHaveProperty('_id', user._id);
        
        await service.remove(user._id)
        
        await expect(service.get(user._id)).rejects.toBeInstanceOf(NotFound);
    })
})

Advanced usage

Use promises and other factories

You're not limited to functions and static data! Your factories can call on other factories to ensure there any relational data is also created for your service.


export const CommentFactory = new Factory(FeathersApp.service('/posts/comments'), {
    // You can use promises
    async content() {
        const response = await Axios.get('https://jsonplaceholder.typicode.com/posts/1/comments');
        return response[0].body;
    },

    // Service depends on a relationship? No problem! Define a `user` factory and call it:
    async userId() {
        return (await Factory.create('user'))._id;
    }
})  

Override properties

You don't need to create a whole new factory if you just need to override one bit of data for a test. For example, if you want to test against comments left by a user with "Gold" membership for example.

const goldUser = await UserFactory.create({ membership: 'gold' });
// { _id: "00000020f51bb4362eee2a4d", username: "Eliseo2", membership: "gold" }

const commentByGoldUser = await CommentFactory.create({
    userId: goldUser._id,
})
// { _id: "507f191e810c19729de860ea", "userId": "00000020f51bb4362eee2a4d", content: "lorem ipsum..." }

Use a property from the current factory

Using this, you can reference properties from the factory result. All properties are promises, so you may need to await the property if you're going to modify it.

This can be particularly if your service depends on relational data belonging to the same entity.

export const OrderFactory = new Factory(FeathersApp.service('/merchant/orders'), {
    customerEmail: () => Faker.internet.email(),
    async merchantId() {
        const merchant = await MerchantFactory.create();
        return merchant._id;
    },
    async productId() {
        const merchantId = await this.merchantId;
        const product = await ProductFactory.create();
        return product._id;
    }
})

See clues.js for more details on how this works.

Setting default service create() params

You can assign default create() params. Handy if your Feathers service hooks rely on some Hook context params for handling the create() requests fired by Feathers-Factory.

export const CommentFacoryWithSlugs = new Factory(FeathersApp.service('/posts/comments'), {
    message: Faker.lorem.sentence,
    async userId() {
        return (await UserFactory.create())._id
    },
}, {
    async query() {
        return {
            postSlug: (await PostFactory.create()).slug
        }
    }
})

Overriding params for a one-off create() call

You can override default service create params. It's worth noting that merging with defaults only goes one level deep.

const SpecialComment = await CommentFactoryWithSlugs.create({}, {
    query: {
        postSlug: 'foo-bar'
    }
});

Creating multiple entries

You can create multiple database entries using the createMany() method. Handy if you need to generate a lot of data for a particular service.

await CommentFactoryWithSlugs.createMany(1337, {}, {
    query: {
        postSlug: 'foo-bar'
    }
});

Only fetch data

You can resolve the factory data without inserting it into the database using the Factory get() method.

await UserFactory.get({ username: 'phantom-user99' });
// { username: "phantom-user99", membership: "bronze" }

Prepare factory result, but don't insert it into the service

You can even use this to fetch the result of your factory methods. Notice merchantId is accessed as if it was a property. See clues.js for more info on how this works.

Factory.create('order', {
    email: Faker.internet.email,
    async merchantId() {
        return (await Factory.create('merchant'))._id;
    },
    async productId() {
        return (await Factory.create('product', { merchantId: await this.merchantId }))
    } 
})

Alternative usage

Define a global factory

Type inference here is not as good as with explicitly exported Factory modules. But can be a good fallback if the schema provided by your Feathers service is giving you type issues. It's also pretty handy if you're working in a non-TypeScript environment.

import { GlobalFactories } from 'feathers-factory';

GlobalFactories.define('my-factory', FeathersApp.service('service-to-mock-for'), {
    
    // Define dynamic data. Perfect with Faker.
    email() {
        return Faker.internet.email();
    },
    
    // Or just define static data.
    servicePlan: 'free',
    
});

Run the factory anywhere you need to mock a database entry with random data:

export default async (FeathersApp) => {
    const user = await Factory.create('my-factory');
    
    console.log(user); 
    // -> { _id: "507f191e810c19729de860ea", email: "Damaris8@yahoo.com", servicePlan: "free" }
    
    await FeathersApp.get(user._id)
    // -> { _id: "507f191e810c19729de860ea", email: "Damaris8@yahoo.com", servicePlan: "free" }
};

How does it work?

Pretty simple - any property, function, method or promise you define in the factory specification is resolved whenever you call Factory.create(), keeping your object structure, but using the return type of all properties within your factory.

For example, a factory generator like the following

{ 
    foo: () => 'bar'
    hello: async () => 'world'
    someNumber: 22
}

Resolves to:

{ foo: 'bar', hello: 'world', someNumber: 22 }

The resolved data is then passed directly into your Feathers service through its create() method.

Credit

Thanks to clues.js for providing an excellent library for resolving in-object data.

License

This repository is licensed under the ISC license.

Copyright (c) 2019, Jørgen Vatle.