DevGang
Авторизоваться

Эффективное использование тестовых данных в React

Когда я писал тесты в предыдущих проектах React, я часто сталкивался с проблемой, когда "тестовые данные" дублировались и, в некоторых случаях, имели разные значения атрибутов, особенно при тестировании разных сценариев. Допустим, я пишу спецификацию, в которой пользователь имеет доступ к записи и может создать пост, а другой тест проверяет, не может ли пользователь создать пост, если доступ к записи не предоставлен. Спецификации будут выглядеть следующим образом:

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()
    })
  })
})

Ладно, спецификация получилась слишком длинной. Но давайте сосредоточимся только на этом блоке кода:

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

Таким образом, вы можете представить, что этот код будет обычным "аранжировщиком" данных, поскольку он определяет текущего вошедшего пользователя и необходим для тех спецификаций, которые требуют информацию о вошедшем пользователе. Он может появиться в нескольких местах в моих файлах спецификаций. Допустим, мне это нужно в следующих спецификациях: create_post.test.tsx, update_post.test.tsx, delete_post.test.tsx, view_post.test.tsx и многих других! А что, если требуется новая функция, в которой нужно добавить новый атрибут пользователю? Значит ли это, что мне также нужно обновить упомянутые выше файлы спецификаций?

Решение

Как же решить эту проблему? Навскидку я могу предложить несколько решений.

Пользователь как модуль

Одно из возможных решений - извлечь объект user и поместить его в отдельный модуль, а затем импортировать его в тех случаях, когда он необходим.

// testUtils/fixtures/user.js

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

И затем в спецификации мы можем просто импортировать его.

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

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

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

Но помните, что нам нужно два варианта пользователя. Один, у которого есть posts.write, и другой, у которого его нет. Поэтому мы можем модифицировать его в:

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: [],
}

Вы можете подумать, почему бы просто не скопировать объект user и не переопределить атрибут permissions:

...
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)
  })
...

Да, вы правы. Это тоже работает! На самом деле это также приводит меня ко второму решению, которое я задумал.

Фабричный паттерн

Итак, в этом решении паттерн аналогичен первому решению, однако вместо объекта мы используем функцию для "создания" нашего клиента user.

// testUtils/fixtures/user.js

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

Как вы можете заметить, createUser позволяет нам переопределять определенные атрибуты, которые нам нужны. Например:

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'],
// }

И разница в спецификации может выглядеть так:

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()
    })
  })
})

Кроме того, если мы хотим иметь данные, основанные на "признаках". Например, мы хотим создать пользователя на основе роли, мы можем сделать что-то вроде:

// 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,
  }
}

А потом, когда мы его реализуем:

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']
// }

Заключение

В обоих решениях мы устраняем проблему жесткого кодирования пользовательских данных по всей спецификации. В случае если мы захотим добавить новые атрибуты для поддержки нового запроса, мы будем знать, что изменить их можно только в одном месте.

Я знаю, что фабричный паттерн - не новый паттерн, на самом деле я обнаружил его, когда работал над проектом Ruby on Rails. В то время это был мой первый опыт работы на Ruby on Rails, и мне дали задание (запрос функции). После написания бизнес-логики мне нужно было написать спецификации, и именно тогда я понял, что такое фабричный паттерн и как он эффективен в нашей команде, которую к тому времени мы приняли в команду фронтенда!

Спасибо за прочтение! Счастливого кодинга!

Источник:

#JavaScript #React #Testing
Комментарии
Чтобы оставить комментарий, необходимо авторизоваться

Присоединяйся в тусовку

В этом месте могла бы быть ваша реклама

Разместить рекламу