Git Product home page Git Product logo

factory.ts's Introduction

factory.ts

willryan

A library to ease creation of factories for test data for Typescript

Given an interface or type definition, create a factory for generating test data. Values for each key may be defaulted or be calculated each time based on a sequence number and the values for other keys.

Installation

Install using yarn:

yarn add -D factory.ts

Install using npm:

npm install --save-dev factory.ts

Example

Interface

interface Person {
  id: number;
  firstName: string;
  lastName: string;
  fullName: string;
  age: number;
}

Basic factory

import * as Factory from "factory.ts";

const personFactory = Factory.Sync.makeFactory<Person>({
  id: Factory.each((i) => i),
  firstName: "Bob",
  lastName: "Smith",
  fullName: "Robert J. Smith, Jr.",
  age: Factory.each((i) => 20 + (i % 10)),
});

For each property of Person, you can specify a default value, or call Factory.Sync.each. Factory.Sync.each takes a lambda with a sequence number that is incremented automatically between generating instances of your type (Person in our example).

You can call personFactory.build with a subset of field data (Partial<Person>) to override defaults, and the output will be an object that conforms to Person using the definition specified in makeFactory.

const james = personFactory.build({
  firstName: "James",
  fullName: "James Smith",
});
// { id: 1, firstName: 'James', lastName: 'Smith', fullName: 'James Smith', age: 21 };

const youngBob = personFactory.build({ age: 5 });
// { id: 2, firstName: 'Bob', lastName: 'Smith', fullName: 'Robert J. Smith, Jr.', age: 5 };

You can also call personFactory.build with no arguments to use factory defaults:

const anybody = personFactory.build();

And you can create an array of objects from factory using buildList (with or without the Partial override):

const theBradyBunch = personFactory.buildList(8, { lastName: "Brady" });

To reset the factory's sequence number used by 'build' and 'buildList':

personFactory.resetSequenceNumber();

Extending factories

Occasionally you may want to extend an existing factory with some changes. For example, we might want to create a personFactory that emits a totally random age range:

const anyAgeFactory = personFactory.extend({
  age: Factory.each(() => randomAge(0, 100)), // randomAge(min:number, max:number) => number
});

anyAgeFactory.build(); // { id: 1, ..., age: <random value> };

Extending a Factory creates a new, immutable Factory. Your initial factory remains unchanged.

Derived values

One specific way to extend an existing factory is to make a new factory where one of the keys/properties is determined by other properties. For example. we can use this to specify fullName from firstName and lastName:

const autoFullNameFactory = personFactory.withDerivation2(
  ["firstName", "lastName"],
  "fullName",
  (fName, lName) => `${lName}, ${fName} ${lName}`
);

const jamesBond = autoFullNameFactory.build({
  firstName: "James",
  lastName: "Bond",
});
// { id: 1, firstName: 'James', lastName: 'Bond', fullName: 'Bond, James Bond', age: 21 };

The withDerivation*N* functions consume an array of dependent key names (of length N), then the name of the property to define, then a lambda that takes the appropriate types for arguments based on the values of the dependent keys, and expects a return type that matches the derived key value type. withDerivation1 through withDerivation5 are provided. Ideally these would be overrides, but I have yet to figure out a way to make the Typescript compiler accept this.

Note that any misspelling of dependent or derived key names in a call to withDerivation*N* will result in a compile error - aren't mapped types the best?

Alternatively, if you need to read more than 5 properties, or just don't want to specify dependencies explicitly, withDerivation expects a property key to derive and a lambda that goes from a value of the overall type being built to a value of the type of the dependent property. For our fullName case that would be:

const autoFullNameFactory = personFactory.withDerivation(
  "fullName",
  (person) => `${person.lName}, ${person.fName} ${person.lName}`
);

Personally I prefer to be explicit about the dependent keys, but it doesn't really matter.

Derivations are processed in the order they are defined, and all withDerivation functions produce a new immutable Factory.

If you wish to use an underlying factory's returned value for a given property as input to derive a new value for the property, you can use withSelfDerviation(). This will ensure the key you are deriving has a valid starting value (not the case when using withDerivation).

Finally, you could instantiate a Derived<TOwner,TProperty> for the value of a property inside a Factory.makeFactory definition, but the type inference can't help you as much - you'll have to indicate the type of TOwner and TProperty.

Combining factories

Sometimes you have two factories you want to combine into one. So essentially you have (p: Partial<T>) => T and (p: Partial<U>) => U and you want (p: Partial<T & U>) => T & U. That's what combine() is for.

const timeStamps = Sync.makeFactory({
  createdAt: Sync.each(() => new Date()),
  updatedAt: Sync.each(() => new Date()),
});
const softDelete = Sync.makeFactory({
  isDeleted: false,
});
interface Post {
  content: string;
  createdAt: Date;
  updatedAt: Date;
  isDeleted: boolean;
}
const postFactory: Sync.Factory<Post> = makeFactory({
  content: "lorem ipsum",
})
  .combine(timeStamps)
  .combine(softDelete);

This pattern allows you to create a factory for a common subset of different types and just re-apply it.

Required Properties

Sometimes you may want to generate a type where some properties to be required every time you call build(). The most common example would be non-null foreign keys in a database. In this case, there's no meaningful value generator you can provide, at least not synchronously.

import * as Factory from "factory.ts";

interface Person {
  id: number;
  firstName: string;
  lastName: string;
  fullName: string;
  age: number;
  parent_id: number;
}

const personFactory = Factory.Sync.makeFactoryWithRequired<Person, "parent_id">(
  {
    id: Factory.each((i) => i),
    firstName: "Bob",
    lastName: "Smith",
    fullName: "Robert J. Smith, Jr.",
    age: Factory.each((i) => 20 + (i % 10)),
  }
);

const invalid = personFactory.build(); // compile error - need base item with { parent_id }
const invalid2 = personFactory.build({}); // compile error - need base item with { parent_id }
const invalid3 = personFactory.build({ firstName: "Sue" }); // compile error - need base item with { parent_id }
const valid2 = personFactory.build({ parent_id: 3 });
const valid = personFactory.build({ parent_id: 5, firstName: "Sue" });

Note the use of makeFactoryWithRequired() to specify required keys.

Async Factories

Async factories support all the same methods as sync factories, but you can also provide generators that create Promise instead of T. Consequently each property may or may not use asynchronicity for generation, but the final factory requires only one await.

transform()

Async factories also have a transform() method which can take a function that goes from T => U or from T => Promise<U>. This creates an object with the factory interface for building only, and is meant to be a "last step" transform. The idea is that the output of the last step may be a different type. For example, you may have an Unsaved and a Saved type for database records, so you can pass in your insert(u: Unsaved): Promise<Saved> method and get a factory which will asynchronously build a persisted Saved object.

Pipelines

Async factories can be put into pipeline, where at each step in the pipeline you add one or more top-level keys to an object meant to hold your test data. This feature is designed to help with bootstrapping complex data structures for tests (e.g. several database records).

Each step in the pipeline can accept:

  • a raw object with keys and values to merge into the pipeline data, OR
  • a function (optionally asynchronous) returning the same, and which can depend on all data in the pipeline up to that point, OR
  • an Async Factory (or TransformFactory or FactoryFunc), along with:
    • a partial for the factory, OR
    • a function (optionally synchronous) returning a partial for the factory

As noted above, each step can depend on the previous steps' data to make its contribution, and each step can be asynchronous. In the end you just await on the pipeline, and through the magic of Typescript you have a type-safe object whose keys are all the values you want to use in your test.

See pipeline.spec.ts for an example.

factory.ts's People

Contributors

8ensmith avatar alfaproject avatar dependabot[bot] avatar ebramanti avatar g-rath avatar gmicek avatar jbenjoy2 avatar jskobos avatar killwolfvlad avatar mitchelst avatar sier-vr avatar trevor-leach avatar vunguyenhung avatar willryan avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

factory.ts's Issues

Problem while using PhanomJS

It runs very well with Chrome (karma-chrome-launcher), but I can not configure it to run with PhantomJS (karma-phantomjs-launcher). I get following error:

ng test --browsers PhantomJS --single-run --code-coverage

10% building modules 1/1 modules 0 active22 12 2017 09:57:32.489:INFO [karma]: Karma v1.7.1 server started at http://0.0.0.0:9876/
22 12 2017 09:57:32.489:INFO [launcher]: Launching browser PhantomJS with unlimited concurrency
22 12 2017 09:57:32.505:INFO [launcher]: Starting browser PhantomJS
10% building modules 2/3 modules 1 active ...frontend\src\polyfills.ts(node:10452) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic, see webpack/loader-utils#56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
22 12 2017 09:57:55.991:INFO [PhantomJS 2.1.1 (Windows 7 0.0.0)]: Connected on socket s8C0IxgvYAI_EOalAAAA with id 95541114
PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
SyntaxError: Use of reserved word 'class'
at http://localhost:9876/_karma_webpack_/vendor.bundle.js:1489
PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
SyntaxError: Use of reserved word 'class'
at http://localhost:9876/_karma_webpack_/vendor.bundle.js:1489

PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
SyntaxError: Use of reserved word 'class'
at http://localhost:9876/_karma_webpack_/vendor.bundle.js:1489
PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
SyntaxError: Use of reserved word 'class'
at http://localhost:9876/_karma_webpack_/vendor.bundle.js:1489

PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
TypeError: undefined is not an object (evaluating 'modules[moduleId].call')
at http://localhost:9876/_karma_webpack_/inline.bundle.js:55
PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
TypeError: undefined is not an object (evaluating 'modules[moduleId].call')
at http://localhost:9876/_karma_webpack_/inline.bundle.js:55

PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
TypeError: undefined is not an object (evaluating 'modules[moduleId].call')
at http://localhost:9876/_karma_webpack_/inline.bundle.js:55
PhantomJS 2.1.1 (Windows 7 0.0.0) ERROR
TypeError: undefined is not an object (evaluating 'modules[moduleId].call')
at http://localhost:9876/_karma_webpack_/inline.bundle.js:55

I have following dependencies in package.json:
"dependencies": {
"@angular-devkit/schematics": "0.0.34",
"@angular/animations": "~4.4.6",
"@angular/cdk": "2.0.0-beta.12",
"@angular/common": "~4.4.6",
"@angular/compiler": "~4.4.6",
"@angular/core": "~4.4.6",
"@angular/flex-layout": "2.0.0-beta.9",
"@angular/forms": "~4.4.6",
"@angular/http": "~4.4.6",
"@angular/material": "2.0.0-beta.12",
"@angular/platform-browser": "~4.4.6",
"@angular/platform-browser-dynamic": "~4.4.6",
"@angular/router": "~4.4.6",
"@ngx-translate/core": "~8.0.0",
"core-js": "~2.4.1",
"fluent-ffmpeg": "~2.1.2",
"intl": "~1.2.5",
"lodash": "~4.17.4",
"md2": "0.0.29",
"ng2-google-charts": "~3.3.0",
"rxjs": "~5.5.3",
"zone.js": "~0.8.18"
},
"devDependencies": {
"@angular/cli": "1.4.9",
"@angular/compiler-cli": "~4.4.6",
"@angular/language-service": "~4.4.6",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/node": "~6.0.60",
"codelyzer": "~3.1.1",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "~1.2.1",
"karma-jasmine": "~1.1.1",
"karma-jasmine-html-reporter": "~0.2.2",
"karma-junit-reporter": "~1.2.0",
"karma-phantomjs-launcher": "~1.0.4",
"phantomjs-prebuilt": "~2.1.16",
"protractor": "~5.1.2",
"ts-node": "~3.2.0",
"tslint": "~5.7.0",
"typescript": "~2.4.2"
}

Make argument of Factory.build() to be optional

Hi,
Firstly, this is a very useful package. I usually use this with Faker.js to create fake data, for example:

// todo.factory.ts
export const TodoFactory = Factory.makeFactory<Todo>({
    content: faker.lorem.words(7),
    completed: faker.random.boolean()
});
// todo.seed.ts
const todo = TodoFactory.build({}); // pass empty object to build a Todo with default field.
export const seedTodos = () => TodoModel.create(todo); // pass created Todo to Mongoose model create()

So, when using this with Faker.js, the needs to define override field on Factory.build() is sometimes redundant. Therefore I think it's better to make the argument of build() todo optional.

Placement of this.seqNum incrementation

Hi! Just wondering if there was any reason why this.seqNum++; is placed before the first loop iteration in Factory.build().

As is, it's making the loop start at 1 vs 0, and ignoring the first index of test data. This makes it so our mock data needs to include a null first index, like this:

const currencyData: string[] = [null,  'USE', 'USE'];
const nameData: string[] = [null,  'Afghanistan', 'Albania'];
const priceDisplayRuleData: string[] = [null,  'base_price', 'base_price'];

I've opened a PR with the proposed implementation to address this (#12), let me know what you think!

Combine factories with optional field

Hello,

I'm trying to combine factories for an interface that has an optional field and I'm running into a typescript error.
Using the following example:

const softDelete = Sync.makeFactory({
    isDeleted: false,
})
interface Post {
    content?: string
    isDeleted: boolean
}
const postFactory: Sync.Factory<Post> = Sync.makeFactory({
    content: 'lorem ipsum',
}).combine(softDelete)

TS compilation results in:

TS2322: Type 'Factory<{ content: string; } & { isDeleted: boolean; }, "isDeleted" | "content">' is not assignable to type 'Factory<Post, keyof Post>'.
  The types of 'builder.isDeleted' are incompatible between these types.
    Type 'boolean | Generator<boolean> | Derived<{ content: string; } & { isDeleted: boolean; }, boolean>' is not assignable to type 'boolean | Generator<boolean> | Derived<Post, boolean>'.
      Type 'Derived<{ content: string; } & { isDeleted: boolean; }, boolean>' is not assignable to type 'boolean | Generator<boolean> | Derived<Post, boolean>'.
        Type 'Derived<{ content: string; } & { isDeleted: boolean; }, boolean>' is not assignable to type 'Derived<Post, boolean>'.
          Type 'Post' is not assignable to type '{ content: string; } & { isDeleted: boolean; }'.

Am I missing something or is this a bug?

Enhancement request: Nested partial?

Hi there. Is there a way to enhance the .build method such that you don't have to specify all of the attributes of a nested override?

For instance, it would be nice to be able to override just budget without having to also have to specific name and typeOfFood as shown below:

interface IGroceryStore {
    aisle: {
       name: string;
       typeOfFood: string;
       budget: number;
    }
}

const groceryStoreFactory = factory.makeFactory<IGroceryStore>({
    aisle: {
       name: 'Junk Food Aisle',
       typeOfFood: 'Junk Food',
       budget: 3000
    }
})

const aStore = groceryStoreFactory.build({ // Error: Property 'name' is missing in type '{ budget: number; }
     aisle: {
         budget: 9999
     }
});

Question: How to create a factory outputs tuples?

Currently the builder interface looks like this:

export declare type Builder<T, K extends keyof T = keyof T> = {
    [P in K]: T[P] | Generator<T[P]> | Derived<T, T[P]>;
};

Which seems like only could describe objects. Would it be possible to create a factory generates tuples? Tuple has fixed length ([number, number]), where array has arbitrary length (number[]).

What I'm trying to get is a Factory<[number, number]>. The best version I could get is:

const generator: Generator<[number, number]> = each<[number, number]>(() => [
  faker.datatype.number(),
  faker.datatype.number(),
])
const factory = makeFactory<[number, number]>(generator.build(0))

Which is not randomized, and calling factory.build() gives me an object:

{
  "0": 80479,
  "1": 32384,
  "length": 2
}

Any suggestion?

Feature request: traits!

This idea is inspired by factory_bot. Off the top of my head, it could look something like this:

interface Person {
  id: number
  firstName: string
  lastName: string
  fullName: string
  age?: number
}

const personFactory = Factory.makeFactory<Person>({
  id: Factory.each(i => i),
  firstName: 'Bob',
  lastName: 'Smith',
  fullName: 'Robert J. Smith, Jr.',
}, {
  traits: {
    withAge: {
      age: Factory.each(i => 20 + (i % 10)),
    }
  }
})

Then it could be consumed e.g.: const person = personFactory().traits().withAge() or const person = personFactory().traits("withAge") or similar.

Thoughts?

Update documentation with examples for pipeline

In our project we have chance to generate test data, i want to integrate chancejs with Factory.ts
It looks like I need to use pipeline?
Could you update readme with example for pipeline?

[feature proposal] Allow to specify factory as field default value of another factory

Given that we have following hierarchy of interfaces:

interface Person {
  id: number;
  name: Name
  age: number;
}

interface Name {
  firstName: string;
  lastName: string;
  fullName: string;
}

I propose that it should be possible to define factories in the following way:

const nameFactory = Factory.Sync.makeFactory<Name>({
  firstName: "Bob",
  lastName: "Smith",
  fullName: "Robert J. Smith, Jr.",
});

const personFactory = Factory.Sync.makeFactory<Person>({
  id: 1,
  name: nameFactory,
  age: 10
});

When personFactory .build() is invoked with person data, the corresponding subset is passed to nameFactory.build():

personFactory.build({
  id: 9,
  name: {
    firstName: "John"
  }
});
// { id: 9 , name: { firstName: "John", lastName: "Smith", fullName: "Robert J. Smith, Jr." }, age: 10}

Derivation using the existing value

Hello! ๐Ÿ‘‹

I'm trying to base the value in the derived factory on the value that was built by the original factory, but looks like this isn't possible with withDerivation or withDerivation1? (I attached an example below)

Is there any other way to achieve this? If not, would you consider adding this functionality into the library?

Example
import * as Factory from "factory.ts";

const userFactory = Factory.makeFactory({
  firstName: "Peter",
  lastName: "Parker"
});

// Doesn't work - firstName is an empty object
const userWithMiddleNameFactory = userFactory.withDerivation('firstName', (user) => {
  return user.firstName + " Benjamin"
});

// Also doesn't work
const altUserWithMiddleNameFactory = userFactory.withDerivation1(['firstName'], 'firstName', (firstName) => {
  return firstName + " Benjamin"
});

// Works for different fields
const userWithSameNamesFactory = userFactory.withDerivation('firstName', (user) => {
  return user.lastName
});

console.table({
  userWithMiddleNameFactory: userWithMiddleNameFactory.build(),
  altUserWithMiddleNameFactory: altUserWithMiddleNameFactory.build(),
  userWithSameNamesFactory: userWithSameNamesFactory.build()
});

which prints:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚           (index)            โ”‚         firstName          โ”‚ lastName โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  userWithMiddleNameFactory   โ”‚ '[object Object] Benjamin' โ”‚ 'Parker' โ”‚
โ”‚ altUserWithMiddleNameFactory โ”‚ '[object Object] Benjamin' โ”‚ 'Parker' โ”‚
โ”‚   userWithSameNamesFactory   โ”‚          'Parker'          โ”‚ 'Parker' โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

p.s. This library has been super useful, thanks a lot for making it! ๐Ÿ‘

How to randomize buildList results

BuildList always returns the same result, even though I'm using faker to try to auto generate values. Is there a way this can be randomized?

Example:

const results = myFactory.buildList(3);

Expected

{
[dayOftheWeek: 'Tuesday'],
[dayOftheWeek: 'Wednesday'],
[dayOftheWeek: 'Friday'],
}

Actual

{
[dayOftheWeek: 'Tuesday'],
[dayOftheWeek: 'Tuesday'],
[dayOftheWeek: 'Tuesday'],
}

Getting type error with typescript 3.4.1 (and later) using combine

This is a minimal example created from your documentation for combine().

I added the const viewType to hover over in vscode to view type.

I appear to be getting a never type output from the combine Keys type.

This is a demo of the problem https://github.com/rluiten/demo-factory.ts

Example demo.ts that is in

import * as Factory from 'factory.ts';

const timeStamps = Factory.Sync.makeFactory({
  createdAt: Factory.Sync.each(() => new Date()),
  updatedAt: Factory.Sync.each(() => new Date())
});

const softDelete = Factory.Sync.makeFactory({
  isDeleted: false
});

interface Post {
  content: string;
  createdAt: Date;
  updatedAt: Date;
  isDeleted: boolean;
}

export const postFactory: Factory.Sync.Factory<Post> = Factory.Sync.makeFactory({
  content: 'lorem ipsum'
})
  .combine(timeStamps)
  .combine(softDelete);


const viewType = timeStamps.combine(softDelete)

Example of error running npm t

ฮป npm t

> [email protected] test C:\dev\tmp\demo-factory.ts
> tsc demo.ts

demo.ts:19:14 - error TS2322: Type 'Factory<{ content: string; } & { createdAt: Date; updatedAt: Date; } & { isDeleted: boolean; }, never>' is not assignable to type 'Factory<Post, "createdAt" | "updatedAt" | "isDeleted" | "content">'.
  Type '"createdAt" | "updatedAt" | "isDeleted" | "content"' is not assignable to type 'never'.
    Type '"createdAt"' is not assignable to type 'never'.

19 export const postFactory: Factory.Sync.Factory<Post> = Factory.Sync.makeFactory({
                ~~~~~~~~~~~


Found 1 error.

npm ERR! Test failed.  See above for more details.

[Question] Can we nested the factory ?

Thanks for your contribution to this project. It's very helpfull.

So there is the question:

interface Inner {
   innerString: string
}
interface Outer {
  innerObject: Inner
  OuterString: string
}

I need two factory one for Inner and another for Outer. Could we use the Inner factory inside outer? like this:

const InnerFactory = Factory.Sync.makeFactory<Inner>({
  innerString: "mock string"
}}

const OuterFactory = Factory.Sync.makeFactory<Outer>({
  innerObject: InnerFactory.build()
  OuterString: "mock string"
})

Honestly, I don't like to nest it. But I can't find a reason why we should not do that.

Thanks.

Can't type "bindBuild" method w/ TS 3.5.2

I've been doing the following w/ my factories:

interface Mx {
  prop1: string;
  prop2: number;
}

const MxFactory = makeFactory<Mx>({
  prop1: '',
  prop2: 1
});

const bindBuild = <T, K extends keyof T, F extends Factory<T, K>>(factory: F): F['build'] => factory.build.bind(factory);

export const buildMx = bindBuild(MxFactory);

Typically each factory lives in their own file, and then the above is done in factories/index, to make it nicer to call build. The idea is that we generally only need to call build most of the time when using factories on our tests - if we need to use the factory itself, we can import it directly.

This worked fine, until I updated to typescript 3.5.x, where the return value becomes an error:

Error:(13, 94) TS2322: Type '(...args: (RecPartial<T> & Pick<T, Exclude<keyof T, K>>)[]) => T' is not assignable to type 'F["build"]'.
  Type '(...args: (RecPartial<T> & Pick<T, Exclude<keyof T, K>>)[]) => T' is not assignable to type 'FactoryFunc<T, K>'.

This seems to be b/c FactoryFunc is a conditional, which is interesting b/c there doesn't seem to be any breaking changes relating to conditional types in 3.5.x

I know what I'm doing isn't strictly within the scope of the library itself, but was hoping you might be able to provide additional insight on what the problem could be, as you understand the types far better than me.

I can use MxFactory.build.bind(MxFactory) directly, but it'd be great if I could get this typed.

I've created an example repo here.

I have tried playing around w/ the types a fair bit, including applying the extends {} workaround for the new generic breaking change, replacing Omit w/ the new loose Omit thats now included in TS, and renaming Omit to StrictOmit in case they were clashing. The only thing that actually worked was removing the conditional :/

An import path cannot end with a '.ts' extension

Hey! First of all, thanks for a great library :)

I was just working on getting this set up in my project, and I get an error when I try to import the library in a typescript file:
import * as Factory from 'factory.ts';

The compiler says:
error TS2691: An import path cannot end with a '.ts' extension. Consider importing 'factory' instead.

I am using Typescript 2.4.2 and library version 0.2.2.

Is there something I am missing in the tsconfig? How can I get past this error?
Thanks!

Node version bump causes install to fail

Hello! I just wanted to give you a heads up that anyone running an older version of node may run into install failures with the latest version of factory.ts (v0.5.2). Anyone who uses yarn without --ignore-engines or npm with engine-strict set to true, won't be able to install the latest unless they are running node v14 or later.

Node v12 is still pretty common, so I imagine others will run into this.

Random builder

This might be a bit meta, but is it possible to get a Factory instance that's going to produce random (but correctly typed) objects ?

I'd like to:
Factory.makeFactory<Foo>().build() and get a completely random Foo

This library does exactly this in Java: https://github.com/benas/random-beans

Current state of the library

Hi @willryan ,

Thanks for this great library. Is it actively maintained?

I would like to help maintain it - im using it for several projects and there are functionalities I would like to add etc.

Default values can not be overridden to null

The following test will fail.

interface ChildType { name: string | null; }
const childFactory = Factory.makeFactory<ChildType>({name: "Kid"});
const anon = childFactory.build({ name: null });
expect(anon.name).to.be.null; 

Failed to parse source maps

Hi @willryan, thank you for your awesome work.

I have been using factory.ts since last year to generate fake data with Faker, with no issues.
However, as I am now using create-react-app v5, I came across warnings related to source maps not being available for factory.ts every time I compile my app.
Currently, they are shown as warnings in development mode obviously, but not sure if they could occur in production as well. I don't know.

The warnings follow this pattern: Failed to parse source map from (files under /src)

For CRA, I found this discussion. Apparently, a lot of folks are using libs that don't expose source maps, and CRA is complaining about that.

Using an env var for CRA like GENERATE_SOURCEMAP=false would eliminate those warnings (for all third parties actually, not just a specific target) however, I don't find it practical to use that. I'd like to enable source maps for factory.ts.

I can see in tsconfig.json that "sourceMap": true is commented under compilerOptions, is there any reason for that? Is that by choice?

My factory.ts version is 1.3.0, and I am using Node.js 18

Possible to allow "transient" values?

FactoryBot for ruby has the notion of transient attributes that are used by the factory but not added to the model. How would one achieve that with factory.ts?

I have some factories that have a complex-ish string that is based on a simple ID but that ID is not part of the model. I don't want factory clients to have to know how to construct the string. I'd like them to be able to call the factory, passing in the id and any other attributes they want to use, and have the factory be able to construct the string out of the ID.

Is this possible in factory.js?

Can't use Factory.each to custimize value of build

I'm playing around with Factory.ts and ran into once obstacle. When I'm building an object, I want to customize a property by using Factory.each().

My build looks like this:

const obj = objMock.build({ prop: propMock
  .buildList(5, { items: Factory.each((i) => ['item1', 'item2', 'item3', 'item4'][i])  })
})

But I get an error, that each does not exist on typeof Factory.

Is this expected, that Factory.each() can only be used when making the Factory?

How to randomise nested object key in `buildList`

BuildList only randomizes the first level key value.

Example:

const factory = Factory.Sync.makeFactory({
    randomItem: Factory.each(()=> radomValueGenerator())  // using faker-js
    someObject: {
        randomNestedItem: Factory.each(()=> radomValueGenerator())
    }
})

const results = factory.buildList(3);

Expected

[
    {
        randomItem: "random value 1",
        someObject: {
            randomNestedItem: "random nested value 1",
        }
    },
    {
        randomItem: "random value 2",
        someObject: {
            randomNestedItem: "random nested value 2",
        }
    },
    {
        randomItem: "random value 3",
        someObject: {
            randomNestedItem: "random nested value 3"
        }
    }
]

Actual

[
    {
        randomItem: "random value 1",
        someObject: {
            randomNestedItem: Generator,
        }
    },
    {
        randomItem: "random value 2",
        someObject: {
            randomNestedItem: Generator
        }
    },
    {
        randomItem: "random value 3",
        someObject: {
            randomNestedItem: Generator
        }
    }
]

Also tried using

const factory = Factory.Sync.makeFactory({
    ...
    someObject: Factory.Sync.makeFactory({
        randomNestedItem: Factory.each(()=> radomValueGenerator())
    }).build()
})

but I get

[
    {
        randomItem: "random value 1",
        someObject: {
            randomNestedItem: "random nested value 1", // same value repeated
        }
    },
    {
        randomItem: "random value 2",
        someObject: {
            randomNestedItem: "random nested value 1", // same value repeated
        }
    },
    {
        randomItem: "random value 3",
        someObject: {
            randomNestedItem: "random nested value 1", // same value repeated
        }
    }
]

Default merging of objects within factories?

Hello!

First off, thanks for making and maintaining this project. We've enjoyed using these factories for improving our test data.

This isn't a bug per-say, but I had a question about something I've noticed while using factory.ts. Specifically, it seems that, when adding nested objects to factories, there is some sort of merging of data that happens with the default provided.

i.e. consider this example:

const personFactory = Factory.makeFactory<Person>({
	firstName: "John",
	lastName: "Doe",
	age: 45,
	extras: {
		occupation: "Accountant",
		country: "USA"
	}
})

Now, let's say I want to OVERRIDE entirely what is stored in extras, with the following example:

const person = personFactory.build({
	extras: {
		married: true,
		weight: 150
	}
})

The problem is, this doesn't actually override what is stored in the default factory, and instead it does some kind of merging to leave me with this result:

{
	firstName: "John",
	lastName: "Doe",
	age: 45,
	extras: {
		occupation: "Accountant",
		country: "USA",
		married: true,
		weight: 150
	}
}

In other words, my custom overridden has been merged into one with the default data.

My question is: what is the motivation behind this and is there anyway to opt out of this? What would be the recommended approach if I wanted to be able to actually override something? I'd hate to make an entirely new factory just for this one case.

Feature request: `resetAllSequenceNumbers`

To make tests independent of one another it is required to reset sequence number. Calling resetSequenceNumber on all used factories might by burdensome or easily oversighted when multiple factories are used in single file. It would be great to have a method like Factory.resetAllSequenceNumbers() which would reset sequence numbers in all factories at once.

Use value from another field

Is it possible to use a field value, from another fields's value?

I.e.

export const someFactory = Factory.Sync.makeFactory<ISomeType>({
  id: uuidv4(),
  detailLink: Factory.each(i => `/${this.id}`)
})

Feature request: pass to makeFactory function that returns Builder instead object Builder

In our project we have parent dto and child dtos which can omit some properties and add some new properties. Would be great to reuse parent dto factory in child dto factory. I think we can allow to pass into makeFactory function that returns Builder instead object Builder to solve this issue.

Example:

import { OmitType } from "@nestjs/swagger";
import { each, makeFactory } from "factory.ts";
import faker from "faker";
import _ from "lodash";

class UserDto {
  public readonly id!: string;
  public readonly version!: number;

  public readonly name!: string;
  public readonly email!: string;
}

class CreateUserDto extends OmitType(UserDto, ["id", "version"]) {}

const userDtoFactory = makeFactory<UserDto>(
  {
    id: each((seqNumber) => String(seqNumber)),
    version: each(() => faker.datatype.number()),

    name: each(() => faker.name.findName()),
    email: each(() => faker.internet.email()),
  },
  {
    startingSequenceNumber: 1,
  },
);

const createUserDtoFactory = makeFactory<CreateUserDto>(() =>
  _.omit(userDtoFactory.build(), "id", "version"),
);

test("example", () => {
  expect(userDtoFactory.build()).toMatchInlineSnapshot(`
    Object {
      "email": "[email protected]",
      "id": "1",
      "name": "Maurice Collins",
      "version": 67633,
    }
  `);

  expect(createUserDtoFactory.build()).toMatchInlineSnapshot(`
    Object {
      "email": "[email protected]",
      "name": "Gustavo Harber",
    }
  `);
});

[Feature request]: makeFactory accepts function parameter

Status quo:

import * as Factory from 'factory.ts'
import Chance from 'chance'
const chance = new Chance()

interface IUser {
  id: number
  username: string
  password: string
  age: number
  createdAt: Date
  updatedAt: Date
}
const UserMock = Factory.Sync.makeFactory<IUser>({
  id: Factory.each(i => i),
  username: Factory.each(() => chance.name()),
  password: Factory.each(() => chance.hash()),
  age: Factory.each(() => chance.integer({ min: 0, max: 120 })),
  createdAt: Factory.each(() => chance.date()),
  updatedAt: Factory.each(() => chance.date())
})

type ILoginReqBody = Pick<IUser, 'username' | 'password'>
const LoginReqBodyMock = Factory.Sync.makeFactory<ILoginReqBody>({
  // I have to build twice here to reuse the logic
  username: Factory.each(() => UserMock.build().username),
  password: Factory.each(() => UserMock.build().password)
})

Proposal:

const LoginReqBodyMock = Factory.Sync.makeFactory<ILoginReqBody>(() => {
  const { username, password } = UserMock.build() // just build once!
  return { username, password } // the return type matches ILoginReqBody
})

Pipelines as generators

I have been working with factory.ts for quite some time and I am a big. But I recently had the issue where I needed to get multiple instances of a Pipeline. I use result of a pipeline as a representations of DBrecords where some entities have dependencies on others and I needed to create multiple of such, but I could not see that this is supported. So I made a generator function that creates a pipeline in each iteration and returns the result as a workaround.

It would be cool if it was a buildin feature to do something like const pipeline = Factory.Pipeline.Pipeline.start().addFac....; await pipeline.buildList(10) or something similar.

Any thoughts?

Required values

Hey, first off good to see a typescript factory library coming together.

Would be cool to have the option to define required props so that you can identify things that should always be provided (perhaps they have relationships to other records, foreign keys, etc). For example:

interface Employee {
  id: number
  name: string
  managerId: number
}

// provide a list of 'required' keys as a type arg
const employeeFactory = Factory.Sync.makeFactory<Employee, 'managerId'>({
  id: Factory.each(i => i),
  name: 'Bob'
  // able to leave out required keys in the definition
})

// this would be typed to require providing `managerId`
const employee = employeeFactory.build({
  managerId: 3
})

// type errors
const employee = employeeFactory.build()

Thoughts?

Weird type error with nested objects with index type

I'm not too sure why this error is happening, and was hoping you could shed some light on if this is a bug with this library, or something I just have to work around:

interface Mx {
  data: {
    prop: string | null;

    [K: string]: unknown;
  };
}

const f = makeFactory<Mx>({ data: { prop: null } });

const f1 = f.build();
const f2 = f.build({
  ...f1,
  data: {
    ...f1.data,
    v: 'hello world'
  }
});

The above code gives the following error:

TS2345: Argument of type '{ data: { v: string; prop: string | null; }; }' is not assignable to parameter of type 'RecPartial<Mx>'.
  Types of property 'data' are incompatible.
    Type '{ v: string; prop: string | null; }' is not assignable to type 'RecPartial<{ [K: string]: unknown; prop: string | null; }>'.
      Property 'prop' is incompatible with index signature.
        Type 'string | null' is not assignable to type 'RecPartial<unknown> | undefined'.
          Type 'null' is not assignable to type 'RecPartial<unknown> | undefined'.

If I change the index type to be any, v becomes the problem:

TS2345: Argument of type '{ data: { v: string; prop: string | null; }; }' is not assignable to parameter of type 'RecPartial<Mx>'.
  Types of property 'data' are incompatible.
    Type '{ v: string; prop: string | null; }' is not assignable to type 'RecPartial<{ [K: string]: any; prop: string | null; }>'.
      Property 'v' is incompatible with index signature.
        Type 'string' is not assignable to type 'RecPartial<any> | undefined'.

The error goes away when I do the following:

  • Remove the ...f1.data
  • Add prop: <string> (but not prop: null)
  • Add prop: undefined
  • Change the signature of prop to string | undefined

None of these work if I use any instead of unknown, nor if I use a subfactory:

const f2 = f.build({
  ...f1,
  data: makeFactory<Mx['data']>({ prop: null }).build({
    ...f1.data,
    v: 'string'
  })
});

Overall, I'm pretty confused by this ๐Ÿ˜‚
Let me know if there's anything else I can provide!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.