Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove unnecessary 'factories' injection #21

Merged
merged 4 commits into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 57 additions & 121 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

[![CircleCI](https://circleci.com/gh/thoughtbot/fishery.svg?style=svg)](https://circleci.com/gh/thoughtbot/fishery)

Fishery is a library for setting up JavaScript objects for use in tests,
Storybook, and anywhere else you need to set up data. It is loosely modeled
after the Ruby gem, [factory_bot][factory_bot].
Fishery is a library for setting up JavaScript objects for use in tests and
anywhere else you need to set up data. It is loosely modeled after the Ruby
gem, [factory_bot][factory_bot].

Fishery is built with TypeScript in mind. Factories accept typed parameters and
return typed objects, so you can be confident that the data used in your tests
Expand Down Expand Up @@ -38,33 +38,22 @@ objects. Here's how it's done:
// factories/user.ts
import { Factory } from 'fishery';
import { User } from '../my-types';
import postFactory from './post';

export default Factory.define<User>(({ sequence, factories }) => ({
export default Factory.define<User>(({ sequence }) => ({
id: sequence,
name: 'Bob',
address: { city: 'Grand Rapids', state: 'MI', country: 'USA' },
posts: factories.post.buildList(2),
name: 'Rosa',
address: { city: 'Austin', state: 'TX', country: 'USA' },
posts: postFactory.buildList(2),
}));
```

### Combine factories

Now combine your factories together and register them:
### Build objects with your factories

```typescript
// factories/index.ts
import { register } from 'fishery';
import user from './user';
import post from './post';

export const factories = register({
user,
post,
});
const user = userFactory.build({ name: 'Sandra' });
```

### Build objects with your factories

Pass parameters as the first argument to `build` to override your factory
defaults. These parameters are deep-merged into the default object returned by
your factory.
Expand All @@ -74,8 +63,7 @@ your factory.
- `transient`: data for use in your factory that doesn't get overlaid onto your
result object. More on this in the [Transient
Params](#params-that-dont-map-to-the-result-object-transient-params) section
- `associations`: often not required but can be useful in the case of
bi-directional associations. More on this in the [Associations](#Associations)
- `associations`: often not required but can be useful in order to short-circuit creating associations. More on this in the [Associations](#Associations)
section

```typescript
Expand Down Expand Up @@ -110,7 +98,7 @@ const user = factories.user.build({ foo: 'bar' }); // type error! Argument of ty
```

```typescript
export default Factory.define<User, Factories, UserTransientParams>(
export default Factory.define<User, UserTransientParams>(
({ sequence, params, transientParams, associations, afterBuild }) => {
params.firstName; // Property 'firstName' does not exist on type 'DeepPartial<User>
transientParams.foo; // Property 'foo' does not exist on type 'Partial<UserTransientParams>'
Expand All @@ -131,61 +119,44 @@ export default Factory.define<User, Factories, UserTransientParams>(

### Associations

If your factory references another factory, use the `factories` object
provided to the factory:
Factories can import and reference other factories for associations:

```typescript
const postFactory = Factory.define<Post, Factories>(({ factories }) => ({
import userFactory from './user';

const postFactory = Factory.define<Post>(() => ({
title: 'My Blog Post',
author: factories.user.build(),
author: userFactory.build(),
}));
```

If you'd like to be able to pass in an association when building your object and
short-circuit the call to `factories.xxx.build()`, use the `associations`
short-circuit the call to `yourFactory.build()`, use the `associations`
variable provided to your factory:

```typescript
const postFactory = Factory.define<Post, Factories>(
({ factories, associations }) => ({
title: 'My Blog Post',
author: associations.author || factories.user.build(),
}),
);
const postFactory = Factory.define<Post>(({ associations }) => ({
title: 'My Blog Post',
author: associations.author || userFactory.build(),
}));
```

Then build your object like this:

```typescript
factories.post.build({}, { associations: { author: susan } });
const jordan = userFactory.build({ name: 'Jordan' });
factories.post.build({}, { associations: { author: jordan } });
```

#### Typing the `factories` factory argument

In the above examples, the `Factories` generic parameter is passed to
`define`. This is optional but recommended in order to get type-checking of
the `factories` object. You can define your `Factories` type like this:
If two factories reference each other, they can usually import each other
without issues, but TypeScript might require you to explicitly type your
factory before exporting so it can determine the type before the circular
references resolve:

```typescript
// factories/types.ts
export interface Factories {
user: Factory<User>;
post: Factory<Post>;
}
```

Once you've defined your `Factories` type, it can also be used when
registering your factories. This ensures that your `Factories` type is always
in sync with the actual factories that you have registered:

```typescript
// factories/index.ts
import { register } from 'fishery';
import user from './user';
import post from './post';
import { Factories } from './types';

export const factories: Factories = register({ user, post });
// the extra Factory<Post> typing can be necessary with circular imports
const postFactory: Factory<Post> = Factory.define<Post>(() => ({ ...}));
export default postFactory;
```

### Use `params` to access passed in properties
Expand All @@ -196,7 +167,7 @@ explicitly access the params in your factory. This can, however, be useful,
for example, if your factory uses the params to compute other properties:

```typescript
const userFactory = Factory.define<User, Factories>(({ params }) => {
const userFactory = Factory.define<User>(({ params }) => {
const { name = 'Bob Smith' } = params;
const email = params.email || `${kebabCase(name)}@example.com`;

Expand Down Expand Up @@ -234,13 +205,13 @@ interface UserTransientParams {
numPosts: number;
}

const userFactory = Factory.define<User, Factories, UserTransientParams>(
({ transientParams, factories, sequence }) => {
const userFactory = Factory.define<User, UserTransientParams>(
({ transientParams, sequence }) => {
const { registered, numPosts = 1 } = transientParams;

const user = {
name: 'Susan Velasquez',
posts: factories.posts.buildList(numPosts),
posts: postFactory.buildList(numPosts),
memberId: registered ? `member-${sequence}` : null,
permissions: {
canPost: registered,
Expand All @@ -251,9 +222,8 @@ const userFactory = Factory.define<User, Factories, UserTransientParams>(
```

In the example above, we also created a type called `UserTransientParams` and
passed it as the third generic type to `define`. This isn't required but
gives you type checking of transient params, both in the factory and when
calling `build`.
passed it as the second generic type to `define`. This gives you type
checking of transient params, both in the factory and when calling `build`.

When constructing objects, any regular params you pass to `build` take
precedence over the transient params:
Expand All @@ -278,20 +248,18 @@ This can be useful if a reference to the object is needed, like when setting
up relationships:

```typescript
export default Factory.define<User, Factories>(
({ factories, sequence, afterBuild }) => {
afterBuild(user => {
const post = factories.post.build({}, { associations: { author: user } });
user.posts.push(post);
});
export default Factory.define<User>(({ sequence, afterBuild }) => {
afterBuild(user => {
const post = factories.post.build({}, { associations: { author: user } });
user.posts.push(post);
});

return {
id: sequence,
name: 'Bob',
posts: [],
};
},
);
return {
id: sequence,
name: 'Bob',
posts: [],
};
});
```

### Extending factories
Expand All @@ -310,11 +278,12 @@ const admin = adminFactory.build();
admin.admin; // true
```

Factories are immutable, so the extension methods return a new factory with
the specified `params`, `transientParams`, `associations`, or `afterBuild`
added to it. When `build` is called on the factory, the `params`,
`transientParams`, and `associations` are passed in along with the values
supplied to `build`. Values supplied to `build` override these defaults.
The extension methods return a new factory with the specified `params`,
`transientParams`, `associations`, or `afterBuild` added to it and do not
modify the factory they are called on. When `build` is called on the factory,
the `params`, `transientParams`, and `associations` are passed in along with
the values supplied to `build`. Values supplied to `build` override these
defaults.

`afterBuild` just adds a function that is called when the object is built.
The `afterBuild` defined in `Factory.define` is always called first if
Expand Down Expand Up @@ -352,8 +321,8 @@ Factories are just classes, so adding reusable builder methods is as simple
as subclassing `Factory` and defining any desired methods:

```typescript
class UserFactory extends Factory<User, Factories, UserTransientParams> {
admin(adminId: string) {
class UserFactory extends Factory<User, UserTransientParams> {
admin(adminId?: string) {
return this.params({
admin: true,
adminId: adminId || `admin-${this.sequence()}`,
Expand All @@ -364,7 +333,7 @@ class UserFactory extends Factory<User, Factories, UserTransientParams> {
return this
.params({ memberId: this.sequence() })
.transient({ registered: true })
.associations({ profile: factories.profile.build() })
.associations({ profile: profileFactory.build() })
.afterBuild(user => console.log(user))
}
}
Expand All @@ -378,39 +347,6 @@ const user = userFactory.admin().registered().build()
To learn more about the factory builder methods `params`, `transient`,
`associations`, and `afterBuild`, see [Extending factories](#extending-factories), above.

### Defining one-off factories without calling `register`

Factories should usually be defined and then combined together using `register`:

```typescript
// factories/index.ts
import { register } from 'fishery';
import user from './user';
import post from './post';
import { Factories } from './types';

export const factories: Factories = register({ user, post });
```

The factories passed to register get injected into each factory so factories can
access each other. This prevents circular dependencies that could arise if your
factories try to access other factories directly by importing them and also
creates a convenient way for factories to access other factories without having
to explicitly import them.

If you are defining a factory for use in a single test file, you might not wish
to register the factory or use the `factories` object that gets injected to the
factory. In this case, you can use `defineUnregistered` instead of `define` and
then skip calling `register`, eg:

```typescript
const personFactory = Factory.defineUnregistered<Person>(() => ({
name: 'Sasha',
}));

const person = personFactory.build();
```

### Rewind Sequence

A factory's sequence can be rewound with `rewindSequence()`.
Expand Down
49 changes: 11 additions & 38 deletions lib/__tests__/associations.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Factory, register } from 'fishery';
import { Factory } from 'fishery';

interface Post {
title: string;
Expand All @@ -10,35 +10,15 @@ interface User {
posts: Array<Post>;
}

interface Factories {
user: Factory<User>;
post: Factory<Post>;
}

describe('associations', () => {
it('can access named factories object in generator fn', () => {
expect.assertions(2);
const factory = Factory.define<User, Factories>(({ factories }) => {
// TODO: type assertions https://github.com/Microsoft/dtslint#write-tests
expect((factories as any).bla.build).not.toBeUndefined();
expect(factories.post).toBeUndefined();
return {} as User;
});

register({ bla: factory });
factory.build();
});

it('can create bi-directional has-many/belongs-to associations', () => {
const userFactory = Factory.define<User, Factories>(
({ factories, afterBuild, transientParams }) => {
const userFactory = Factory.define<User>(
({ afterBuild, transientParams }) => {
const { skipPosts } = transientParams;

afterBuild(user => {
if (!skipPosts) {
user.posts.push(
factories.post.build({}, { associations: { user } }),
);
user.posts.push(postFactory.build({}, { associations: { user } }));
}
});

Expand All @@ -49,20 +29,13 @@ describe('associations', () => {
},
);

const postFactory = Factory.define<Post, Factories>(
({ factories, associations }) => {
return {
title: 'A Post',
user:
associations.user ||
factories.user.build({}, { transient: { skipPosts: true } }),
};
},
);

register({
user: userFactory,
post: postFactory,
const postFactory = Factory.define<Post>(({ associations }) => {
return {
title: 'A Post',
user:
associations.user ||
userFactory.build({}, { transient: { skipPosts: true } }),
};
});

const user = userFactory.build();
Expand Down
6 changes: 1 addition & 5 deletions lib/__tests__/class-factories.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Factory, HookFn, register } from 'fishery';
import { Factory } from 'fishery';

describe('Using with classes', () => {
class Address {
Expand All @@ -16,10 +16,6 @@ describe('Using with classes', () => {
() => new User('Sharon', new Address('Detroit', 'MI')),
);

register({
user: userFactory,
});

it('works correctly with read-only properties', () => {
const user = userFactory.build();
expect(user).toBeInstanceOf(User);
Expand Down
Loading