habrahabr

Мои «Ого, я этого не знал!» моменты с Jest

  • среда, 26 июня 2019 г. в 00:18:09
https://habr.com/ru/company/otus/blog/457616/
  • Блог компании OTUS. Онлайн-образование
  • JavaScript
  • Программирование


Всем привет! Курс «Разработчик JavaScript» стартует уже в этот четверг. В связи с этим мы решили поделиться переводом еще одного интересного материала. Приятного прочтения.



Jest всегда был моим незаменимым инструментом для юнит-тестирования. Он настолько надежен, что я начинаю думать, что я всегда недоиспользовал его. Хотя тесты работали, со временем я рефакторил их то здесь, то там, потому что не знал, что Jest так может. Каждый раз это новый код, когда я проверяю документацию Jest.

Итак, я собираюсь поделиться некоторыми моими любимыми трюками с Jest, которые некоторые из вас, возможно, уже знают, потому что вы читали документацию, а не как я (позор), но я надеюсь, что это поможет тем, кто лишь наскоро пробегал ее!

Кстати, я использую Jest v24.8.0 в качестве справочного материала, поэтому будьте внимательны, некоторые вещи могут не работать на той версии Jest, которую вы используете в настоящее время. Кроме того, примеры не представляют реальный тестовый код, это просто демонстрация.

#1. .toBe vs .toEqual


Сначала все эти утверждения выглядели нормально для меня:

expect('foo').toEqual('foo')
expect(1).toEqual(1)
expect(['foo']).toEqual(['foo'])

Исходя из использования chai для утверждений равенства (to.equal), это просто естественно. На самом деле, Jest не будет жаловаться, и эти утверждения проходят как обычно.

Тем не менее, Jest имеет .toBe и .toEqual. Первый используется для утверждения равенства с помощью Object.is, а второй — для обеспечения глубокого сравнения объектов и массивов. .toEqual имеет запасной вариант использования Object.is, если выясняется, что не нужно глубокое сравнение, такое как утверждение равенств для примитивных значений, что объясняет, почему предыдущий пример проходил очень хорошо.

expect('foo').toBe('foo')
expect(1).toBe(1)
expect(['foo']).toEqual(['foo'])

Таким образом, вы можете пропустить все if-else в .toEqual, используя .toBe, если вы уже знаете, какие значения вы тестируете.
Распространенной ошибкой является то, что вы будете использовать .toBe для утверждения равенства примитивных значений.

expect(['foo']).toBe(['foo'])

Если вы посмотрите на исходный код при сбое .toBe, он попытается определить, действительно ли вы допустили эту ошибку, вызвав функцию, которая используется .toEqual. Это может быть узким местом при оптимизации вашего теста.

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

expect(Object.is('foo', 'foo')).toBe(true)

Больше деталей в документации.

#2. Более подходящие сравнители


Технически, вы можете использовать .toBe для утверждения любых значений. С Jest вы можете специально использовать определенные средства сравнения, которые сделают ваш тест более читабельным (а в некоторых случаях и более коротким).

// 
expect([1,2,3].length).toBe(3)

// 
expect([1,2,3]).toHaveLength(3)

const canBeUndefined = foo()

// 
expect(typeof canBeUndefined !== 'undefined').toBe(true)

// 
expect(typeof canBeUndefined).not.toBe('undefined')

// 
expect(canBeUndefined).not.toBe(undefined)

// 
expect(canBeUndefined).toBeDefined()

class Foo {
  constructor(param) {
    this.param = param
  
}

// 
expect(new Foo('bar') instanceof Foo).toBe(true)

// 
expect(new Foo('bar')).toBeInstanceOf(Foo)

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

#3. Тестирование с помощью снимков (snapshot-тестирование) на элементах без пользовательского интерфейса


Возможно, вы слышали о тестировании с помощью снимков в Jest, где оно помогает отслеживать изменения в ваших элементах пользовательского интерфейса. Но тестирование снимками этим не ограничивается.

Рассмотрим этот пример:

const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)

expect(happyEmployees[0].nextMonthPaycheck).toBe(1000)
expect(happyEmployees[1].nextMonthPaycheck).toBe(5000)
expect(happyEmployees[2].nextMonthPaycheck).toBe(4000)
// ...etc

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

const allEmployees = getEmployees()
const happyEmployees = giveIncrementByPosition(allEmployees)

expect(happyEmployees).toMatchSnapshot()

Когда бы ни возникали регрессии, вы бы точно знали, какое дерево в узле не соответствует снимку.

Но удобство имеет свою цену: этот метод более подвержен ошибкам. Есть вероятность, что вы не будете знать, что снимок на самом деле неверен, и в конечном итоге вы все равно его совершите. Итак, дважды проверяйте ваш снимок, как будто это ваш собственный код утверждения (потому что это так).

Конечно, на этом тестирование снимками не ограничивается. Читайте полную информацию в документации.

#4. describe.each и test.each


Вы писали когда-нибудь тест, который чем-то похож на этот?

describe('When I am a supervisor', () => {
  test('I should have a supervisor badge', () => {
    const employee = new Employee({ level: 'supervisor' })

    expect(employee.badges).toContain('badge-supervisor')
  })

  test('I should have a supervisor level', () => {
    const employee = new Employee({ level: 'supervisor' })

    expect(employee.level).toBe('supervisor')
  })
})

describe('When I am a manager', () => {
  test('I should have a manager badge', () => {
    const employee = new Employee({ level: 'manager' })

    expect(employee.badges).toContain('badge-manager')
  })

  test('I should have a manager level', () => {
    const employee = new Employee({ level: 'manager' })

    expect(employee.level).toBe('manager')
  })
})

Это монотонно и рутинно, верно? Представьте, что делаете это с большим количеством случаев.
С помощью description.each и test.each вы можете сжать код следующим образом:

const levels = [['manager'], ['supervisor']]
const privileges = [['badges', 'toContain', 'badge-'], ['level', 'toBe', '']]

describe.each(levels)('When I am a %s', (level) => {
  test.each(privileges)(`I should have a ${level} %s`, (kind, assert, prefix) => {
    const employee = new Employee({ level })

    expect(employee[kind])[assert](`${prefix}${level}`)
  })
})

Тем не менее, мне еще предстоит использовать это в моем собственном тесте, поскольку я предпочитаю, чтобы мой тест был подробным, но я просто подумал, что это был бы интересный трюк.

Смотрите документацию для более подробной информации об аргументах (спойлер: табличный синтаксис действительно классный).

#5. Единичная имитация глобальных функций


В какой-то момент вам придется протестировать что-то, что зависит от глобальных функций в конкретном тестовом случае. Например, функция, которая получает информацию о текущей дате, используя Javascript-объект Date, или библиотека, которая опирается на нее. Сложность в том, что если речь идет о текущей дате, вы никогда не сможете получить правильное утверждение.

function foo () {
  return Date.now()
}

expect(foo()).toBe(Date.now())
//  This would throw occasionally:
// expect(received).toBe(expected) // Object.is equality
// 
// Expected: 1558881400838
// Received: 1558881400837


В конце концов, вам бы пришлось переопределить глобальный объект Date, чтобы он был согласованным и управляемым:

function foo () {
  return Date.now()
}

Date.now = () => 1234567890123

expect(foo()).toBe(1234567890123) // 

Однако это считается плохой практикой, поскольку переопределение сохраняется между тестами. Вы не заметите этого, если нет другого теста, основанного на Date.now, но это тоже протечет.

test('First test', () => {
  function foo () {
    return Date.now()
  

  Date.now = () => 1234567890123

  expect(foo()).toBe(1234567890123) // 
})

test('Second test', () => {
  function foo () {
    return Date.now()
  

  expect(foo()).not.toBe(1234567890123) //  ???
})

Раньше я “взламывал” его так, чтобы он не протекал:

test('First test', () => {
  function foo () {
    return Date.now()
  

  const oriDateNow = Date.now
  Date.now = () => 1234567890123

  expect(foo()).toBe(1234567890123) // 
  Date.now = oriDateNow
})

test('Second test', () => {
  function foo () {
    return Date.now()
  

  expect(foo()).not.toBe(1234567890123) //  as expected
})

Тем не менее, есть гораздо лучший, менее хакерский способ сделать это:

test('First test', () => {
  function foo () {
    return Date.now()
  

  jest.spyOn(Date, 'now').mockImplementationOnce(() => 1234567890123)

  expect(foo()).toBe(1234567890123) // 
})

test('Second test', () => {
  function foo () {
    return Date.now()
  

  expect(foo()).not.toBe(1234567890123) //  as expected
})

Таким образом, jest.spyOn следит за глобальным объектом Date имитирует реализацию функции now только для одного вызова. Это, в свою очередь, оставит Date.now нетронутым для остальных тестов.

Есть определенно больше информации на тему заглушек в Jest. Смотрите полную документацию для более подробной информации.

Эта статья становится становится достаточно длинной, так что, думаю, на этом пока все. Это затрагивает лишь малую часть возможностей Jest, и я просто выделяю свои любимые. Если у вас есть другие интересные факты, дайте мне знать.

А также, если вы часто использовали Jest, посмотрите Majestic, который представляет собой графический интерфейс без конфигов для Jest, действительно хорошая альтернатива скучному выводу терминала. Я не уверен, есть ли автор в dev.to, но тем не менее уважение этому человеку.

Как всегда, спасибо за внимание!

На этом все. До встречи на курсе.