Unlocking Test Data Efficiency in React
Tags: react, javascript, jest, unittest
Published: 2024-01-16 16:49:43

When I was writing tests in previous React projects, I often faced this problem where "test data" was duplicated and, in some cases, had different attribute values, especially when testing different scenarios. For example, let's say I am writing a spec where a user can create a post and has a write access and another test to verify that the user cannot create a post when no write access is given. The specs will look as follows:

```javascript
describe('user', () => {
  let handlers = []
  let user

  describe('has write posts permission', () => {
    beforeAll(() => {
      user = {
        id: 1000,
        name: 'John Doe',
        permissions: ['posts.write'],
      }

      handlers = [
        http.get('/me', () => {
          return HttpResponse.json(user)
        }),
      ]

      server.use(...handlers)
    })

    test('can create post', async () => {
      render(<CreatePostPage />)

      fireEvent.change(
        screen.getByRole('textbox', { name: /title/i }),
        {
          target: { value: 'My awesome post' },
        },
      )

      fireEvent.click(
        screen.getByRole('button', { name: /create post/i }),
      )

      expect(
        await screen.findByText(/post created/i)
      ).toBeInTheDocument()
    })
  })

  describe('has no write permission', () => {
    beforeAll(() => {
      user = {
        id: 1000,
        name: 'John Doe',
        permissions: [],
      }

      handlers = [
        http.get('/me', () => {
          return HttpResponse.json(user)
        }),
      ]

      server.use(...handlers)
    })

    test('cannot create post', async () => {
      render(<CreatePostPage />)

      expect(
        await screen.findByText(/the page you're trying access has restricted access./i)
      ).toBeInTheDocument()
    })
  })
})
```

Okay, the spec got too long. But let's focus only on this code block:

```javascript
user = {
  id: 1000,
  name: 'John Doe',
  permissions: ['posts.write'],
}
```

So you can imagine that this code will be common "arrange" data since it defines the currently logged-in user and is necessary for those specs that require logged-in user information. This can appear in multiple places in my spec files. Let's say I need this in the following specs: `create_post.test.tsx`, `update_post.test.tsx`, `delete_post.test.tsx`, `view_post.test.tsx`, and more! What if there is a new feature requirement where I need to add a new attribute to the user? Does that mean I also need to update the mentioned spec files above?

## The solution
So how can we solve this problem? At the top of my mind, I can think of a couple of solutions.

### User as module
One possible solution is we can extract the `user` object and put it in a separate module and then import it on the specs that it is needed.

```javascript
// testUtils/fixtures/user.js

export const user = {
  id: 1000,
  name: 'John Doe',
  permissions: ['posts.write'],
}
```

and then in the spec, we can just import it.

```javascript
...
import { user } from 'testUtils/fixtures/user'

...
  beforeAll(() => {
    handlers = [
      http.get('/me', () => {
        return HttpResponse.json(user)
      }),
    ]

    server.use(...handlers)
  })
...
```

But remember, we need two variants of user. One that has `posts.write` and the other one that does not have. So we can modify it into:
```javascript
export const user_with_posts_write_access = {
  id: 1000,
  name: 'John Doe',
  permissions: ['posts.write'],
}

export const user_without_permissions = {
  id: 1000,
  name: 'John Doe',
  permissions: [],
}
```

You might be thinking, why not just copy the `user` object and override the `permissions` attribute:

```javascript
...
import { user } from 'testUtils/fixtures/user'

...
  beforeAll(() => {
    const user_without_permissions = {
      ...user,
      permissions: [],
    }

    handlers = [
      http.get('/me', () => {
        return HttpResponse.json(user_without_permissions)
      }),
    ]

    server.use(...handlers)
  })
...
```

Yes, you are right. This works too! In fact this also leads me to the second solution I have in mind.

### Factory
So in this solution, the pattern is similar to the first solution however, instead of object, we use a function to "build" our `user`.

```javascript
// testUtils/fixtures/user.js

export function createUser(attributes = {}) {
  return {
    id: 1000,
    name: 'John Doe',
    permissions: ['posts.write'],
    ...attributes,
  }
}
```

As you may notice, the `createUser` allows us to override specific attributes that we need. E.g.

```javascript
createUser()
// Returns
// {
//   id: 1000,
//   name: 'John Doe',
//   permissions: ['posts.write']
// }

createUser({ permissions: [] })
// Returns
// {
//   id: 1000,
//   name: 'John Doe',
//   permissions: []
// }

createUser({ id: 1001, name: 'Jane Doe' })
// Returns
// {
//   id: 1001,
//   name: 'Jane Doe',
//   permissions: ['posts.write'],
// }
```

And the difference on the spec may look like:

```diff
describe('user', () => {
  let handlers = []
  let user

  describe('has write posts permission', () => {
    beforeAll(() => {
-     user = {
-       id: 1000,
-       name: 'John Doe',
-       permissions: ['posts.write'],
-     }
+     user = createUser()

      handlers = [
        http.get('/me', () => {
          return HttpResponse.json(user)
        }),
      ]

      server.use(...handlers)
    })

    test('can create post', async () => {
      render(<CreatePostPage />)

      fireEvent.change(
        screen.getByRole('textbox', { name: /title/i }),
        {
          target: { value: 'My awesome post' },
        },
      )

      fireEvent.click(
        screen.getByRole('button', { name: /create post/i }),
      )

      expect(
        await screen.findByText(/post created/i)
      ).toBeInTheDocument()
    })
  })

  describe('has no write permission', () => {
    beforeAll(() => {
-     user = {
-       id: 1000,
-       name: 'John Doe',
-       permissions: [],
-     }
+     user = createUser({ permissions: [] })

      handlers = [
        http.get('/me', () => {
          return HttpResponse.json(user)
        }),
      ]

      server.use(...handlers)
    })

    test('cannot create post', async () => {
      render(<CreatePostPage />)

      expect(
        await screen.findByText(/the page you're trying access has restricted access./i)
      ).toBeInTheDocument()
    })
  })
})
```

Furthermore, if we want to have "traits"-based data. E.g. we want to create user based on role, we can do something like:

```javascript
// testUtils/fixtures/user.js

export const USER_TRAITS = {
  ADMIN: "admin",
  AUTHOR: "author",
  READER: "reader",
}

export function createUser(trait = USER_TRAITS.READER, attributes = {}) {
  let roleBasedAttributes = {}

  if (trait == USER_TRAITS.ADMIN) {
    roleBasedAttributes = {
      role: 'Admin',
      permissions: ['posts.write', 'posts.read'],
    }
  }

  if (trait == USER_TRAITS.AUTHOR) {
    roleBasedAttributes = {
      role: 'Author',
      permissions: ['posts.write'],
    }
  }

  if (trait == USER_TRAITS.READER) {
    roleBasedAttributes = {
      role: 'Reader',
      permissions: ['posts.read'],
    }
  }

  return {
    id: 1000,
    name: 'John Doe',
    ...roleBasedAttributes,
    ...attributes,
  }
}
```

And then when we implement it:

```javascript
import { createUser, USER_TRAITS } from 'testUtils/fixtures/user.js'

createUser()
// Returns
// {
//   id: 1000,
//   name: 'John Doe',
//   role: 'Reader',
//   permissions: ['posts.read']
// }

createUser(USER_TRAITS.ADMIN, { name: 'Jane Doe' })
// Returns
// {
//   id: 1000,
//   name: 'Jane Doe',
//   role: 'Admin',
//   permissions: ['posts.write', 'posts.read']
// }
```

## Conclusion
In both solutions, we eliminate the problem of having `user` data hardcoded all over the specs. In case we want to add new attributes to support new feature request, we know that there is only one place to change it.

I'm aware that factory pattern is not a new pattern in fact I discover this when I was working on a Ruby on Rails project. At the time, it was my first experience working on Ruby on Rails, and I was given a task (feature request). After writing the business logic, I need to write specs and that was the time I understand the factory pattern and how it is effective in our team. Which by then we adopted on the frontend team!