Setting up reusable factories for test scenarios

We recently started with testing in Redwood, and wanted to easily populate sample data in our scenarios to save time creating them. Hope this helps anyone else looking to simplify how they define / populate test data.

I’ve been using Factory Bot religiously in almost every Rails project I work on, and wanted to have something similar in our Redwood test suite. In each service, we define a service.factory.ts file which essentially returns an object with pre-populated data using fakerjs.dev. A simple factory would like this:

# api/src/services/posts/posts.factory.ts

import { faker } from '@faker-js/faker'

export type PostFactoryInput = {
  name?: string
}

export const postFactory = ({
  name = faker.company.catchPhrase
}: PostFactoryInput) => {
  return {
    name: name
  }
}

The postFactory will just return an object with a name key and value, but we can also set the name to something specific if we need to by passing a name to our factory:

postFactory({ name: 'Cool name!' })

Now to using it in our scenarios. In our posts.scenarios.ts, instead of having to define sample data, we can simplify it to something like this:

# api/src/services/posts/posts.scenarios.ts

import { postFactory } from './posts.factory'

export const standard = defineScenario<Prisma.CreatePostArgs>({
  post: {
    one: {
      data: {
        ...postFactory({}) // Just populate the post using fakerjs
      }
    },
    two: {
      data: {
        ...postFactory({ name: 'Hey there.' }) // Explicitly set the name
      }
    }
  }
})

We can also re-use the factory in other scenarios. Say our posts had an author, and we want to populate authors with some posts. We can setup another factory in authors.factory.ts:

# api/src/services/authors/authors.factory.ts

import { faker } from '@faker-js/faker'

export type AuthorFactoryInput = {
  email?: string
}

export const postFactory = ({
  email = faker.internet.email
}: AuthorFactoryInput) => {
  return {
    email: email
  }
}

then in our authors.scenarios.ts, we can use both factories to populate authors with posts without needing to explicitly define values.

# api/src/services/authors/authors.scenarios.ts

author: {
  one: {
    data: {
      ...authorFactory({}),
      posts: {
        create: {
          ...postFactory({})
        }
      }
    }
  }
  ...
}

Obviously factories are a bit of overkill if your models were as simple as the example ones above, but as our models grow, we can limit the amount of updating we need to do whenever we add new attributes.

Our factories also aren’t limiting to using them in scenarios. We use them in our tests as well, mainly for create and update actions. For example, instead of explicitly defining every value when testing creating a record, we can use the factory to populate attributes for us:

# api/src/services/posts/posts.test.js

scenario('creates a post', async (scenario: StandardScenario) => {
  const attributes = postFactory({})

  const result = await createPost({
    input: {
      ...attributes 
    }
  })

  expect(result.name).toEqual(attributes.name)
}

It’s a pretty simple setup that I think will save us a bunch of time as our app grows in scope. My next step will be to use these factories in my seeds, but haven’t worked out the best way to do that yet.

Anyway hope this helps anyone looking into testing in Redwood and ways to populate test data!

2 Likes

@crabbits I, too, used to use the factory_bot (well, it was called factory_girl back then h/t Andy Warhol) and love this pattern and idea.

We shared with the Core Team and eager to get @rob and @peterp take on it.

Peter’s Snaplet offers copycat to return deterministic faker data that might work well here too.

1 Like

Ah yes factory_girl takes me back. Thanks for the links @dthyresson, I still owe you a post on the Maizzle setup as well, been flat out will get to it soon.

1 Like

We had a few people try out Maizzle and reported only good things back. Thanks for the tip!