x
 
Василий
23 января 2020
Советы почтой каждую неделю
Пожалуйста, получите наше письмо, чтобы подтвердить свой адрес:
Вы подписаны на «Советы за неделю»:

Третья часть: как тестировать модуль, использующий другой модуль


Автотесты «на пальцах»

В про­шлом совете я рас­ска­зы­вал о функ­циях‑запро­сах, функ­циях‑коман­дах и о том, как про­ве­рять их: у запро­сов про­ве­ряем воз­вра­ща­е­мое зна­че­ние, у команд — послед­ствия. Сегодня рас­скажу о тести­ро­ва­нии моду­лей, активно исполь­зу­ю­щих дру­гие внеш­ние модули.

Автотесты «на пальцах»

Модули редко суще­ствуют изо­ли­ро­ванно друг от друга. Чаще всего они зави­сят от дру­гих моду­лей. Напри­мер, обра­ща­ются к ним за какими‑то данными:

«Кор­зина» спра­ши­вает сумму у «Cписка товаров»

«Про­ект» про­сит «Спи­сок задач» вер­нуть только открытые

«Спи­сок поль­зо­ва­те­лей» исполь­зует АПИ‑кли­ент, чтобы загру­зить дан­ные о поль­зо­ва­те­лях с сервера

Или про­сят что‑то сделать:

«Кор­зина» про­сит «Покупку» начать оплату

«Про­ект» про­сит «Спи­сок поль­зо­ва­те­лей» выдать кому‑то права администратора

«Спи­сок поль­зо­ва­те­лей» про­сит АПИ‑кли­ент уда­лить пользователя

Если тести­ро­вать такие модули в лоб, будут полу­чаться неизо­ли­ро­ван­ные, хруп­кие и мед­лен­ные тесты: при каж­дом испы­та­нии будет рабо­тать и тести­ру­е­мый код, и модули, к кото­рым он обра­ща­ется. Чтобы тести­ро­вать модули в отрыве от их зави­си­мо­стей, исполь­зуют заглушки — модули‑дуб­лёры, кото­рые как кас­ка­дёры в кино под­ме­няют реаль­ные модули системы во время тестов. Покажу на примерах.

Дублёры вместо команд

При архи­ва­ции про­екта мы «кон­сер­ви­руем» его тудушки и ком­мен­та­рии и упа­ко­вы­ваем их в архив. С помо­щью тестов нужно убе­диться, что архи­ва­ция рабо­тает: про­ект, его тудушки и ком­мен­та­рии упа­ко­вы­ва­ются в один архив

class Project {
  archive() {
    const project = this
    ​
    Archive.add(this.todos, { project })
    Archive.add(this.comments, { project })
    return Archive.pack({ project })
  }
}

Мы можем напря­мую про­ве­рить, что тудушки и ком­мен­та­рии архи­ви­ру­ются, прой­дясь по ним и све­рив даты, ста­тусы и про­чие детали реа­ли­за­ции архи­ви­ро­ва­ния. Полу­чится полу‑инте­гра­ци­он­ный тест. Если при этом функ­ции архи­ва­ции имеют соб­ствен­ные тесты, полу­чим бес­по­лез­ную тав­то­ло­гию: про­ве­ряем одно и то же в двух раз­ных тестах.

Чтобы не делать двой­ной работы и про­ве­рить, что тудушки и ком­мен­та­рии всё‑таки архи­ви­ру­ются, лучше про­ве­рить факт вызова нуж­ных функций:

import Project from './project'
import Account from './account'
import Archive from './archive'
​
// Под­ме­няем модуль с архи­вом заглуш­кой
jest.mock('./archive')
​
describe('#archive', () => {
  beforeEach(() => {
    // Ста­вим заглушки вме­сто реаль­ных функ­ций add и pack в Archive
    Archive.add.mockReturnValue(true)
    Archive.pack.mockReturnValue(true)
    ​
    // Под­го­тав­ли­ваем нуж­ные зави­си­мо­сти
    const account = new Account({ title: 'Stark Inc.' })
    const project = new Project({ account, todos, comments })
  })
  ​
  it('archives project todos', () => {
    project.archive()
    ​
    // Про­ве­ряем
    expect(Archive.add).toHaveBeenCalledWith(todos, { project })
  })
  ​
  it('archives project comments', () => {
    project.archive()
    ​
    // Про­ве­ряем
    expect(Archive.add).toHaveBeenLastCalledWith(comments, { project })
  })
  ​
  it('packs project archive', () => {
    project.archive()
    ​
    expect(Archive.pack).toHaveBeenCalledWith({ project })
  })
})

Дублёры вместо запросов

Рас­смот­рим дру­гую ситу­а­цию: модуль обра­ща­ется по ХТТП в сто­рон­ний АПИ за номе­ром карты поль­зо­ва­теля и фор­ма­ти­рует его.

import axios from 'axios'
​
const formatCardNumber = (card) => {
  if (!card) return null
  ​
  return card
    .replace(/([0-9]{4})(.*)([0-9]{4})/, '$2···$3')
}
​
class CurrentUser {
  static cardNumber() {
    return axios
      .get('/user/card.json')
      .then(resp => resp.data)
      .then(formatCardNumber)
  }
}
​
export default CurrentUser

Чтобы тест cardNumber был ста­биль­ным и быст­рым, лучше не делать запро­сов к внеш­нему АПИ:

import CurrentUser from './currentUser'
import axios from 'axios'
​
// Под­ме­няем модуль, рабо­та­ю­щий с АПИ, заглуш­кой
jest.mock('axios')
​
describe('CurrentUser', () => {
  describe('.cardNumber', () => {
    describe('when user has card', () => {
      it('returns formatted card number', () => {
        const response = { data: '1234....7777' }
        ​
        // Ста­вим заглушку, кото­рая все­гда воз­вра­щает нуж­ный нам ответ
        axios.get.mockResolvedValue(response)
        ​
        // Про­ве­ряем резуль­тат
        return CurrentUser.cardNumber()
          .then(cardNumber => expect(cardNumber).toEqual('1234···7777'))
      })
    })
    ​
    describe('when user has no card', () => {
      it('returns nothing', () => {
        const response = { data: null }
        axios.get.mockResolvedValue(response)
        ​
        return CurrentUser.cardNumber()
          .then(cardNumber => expect(cardNumber).toBeNull())
      })
    })
  })
})

В общем виде я руко­вод­ству­юсь таким пра­ви­лом: если тест зави­сит от внеш­ней функ­ции‑запроса, исполь­зую вме­сто неё заглушку; если тест зави­сит от внеш­ней функ­ции‑команды, исполь­зую вме­сто неё заглушку и про­ве­ряю факт пра­виль­ного её использования.

При этом из послед­него пра­вила бывают исклю­че­ния: если исполь­зу­е­мые внеш­ние функ­ции‑команды редко меня­ются и их легко про­ве­рить, лучше сэко­но­мить время и про­ве­рить их эффекты напря­мую, без заглушек.

Ещё по теме

P. S. Это был совет о веб‑разработке. Хотите знать всё о коде, тестах, фронтенд‑разработке, цеэсэсе, яваскрипте, рельсах и джейде? Присылайте вопросы.
Вёрстка и прототипирование — дисциплина Школы дизайнеров. Набор открыт. Чем раньше поступите, тем ниже стоимость и выше шанс на бесплатное место.
 

Поделиться
Отправить

Комментарии

Дима Шишикин
23 января 2020

При моке модулей часто нужно заменить настоящую функциональность на фейковую, потому что в юнит-тестах зачастую важен сам факт использования модуля, а не конкретная его реализация.

Например, в случае с axios можно было бы сделать так:

javascript jest.mock('axios', () => { const original = jest.requireActual('axios'); return { ...original, __esModule: true, get: jest.fn(() => ({ data: '1234....7777' })), }; });

Так же любая функция, обернутая в jest.fn() имеет метод mockImpementation(), который позволяет сделать тоже самое.

Например:

import axios from 'axios';
jest.mock('axios');

axios.mockImpementation(() => ({ data: '1234….7777' }))

Такая возможность может пригодится при тестировании.

Владимир Родкин
13 февраля 2020

А если у модуля, от которого зависит тестируемый, меняется интерфейс метода, как это отследить? Ведь ошибки не будет.
Например был такой: Archive.add(this.comments, { project }), а в новой версии такой Archive.add({ project }, this.comments, true, null, 4)
Наш тест пройдёт: expect(Archive.add).toHaveBeenLastCalledWith(comments, { project })


Цель рубрики — обсуждение вопросов дизайна всех видов, текста в дизайне и взаимоотношений дизайнеров с клиентами.

Мы публикуем комментарии, которые добавляют к уже сказанному новые мысли и хорошие примеры. Мы ожидаем, что такие комментарии составят около 20% от общего числа.

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

Вот такой веб 2.0.

Какую библиотеку вы используете при смещении фона? Как быть, если всё моё время уходит на разработку всё новых и новых фич? Типовые решения в вёрстке. Как сверстать простую шапку страницы с меню 4 Как написать аккуратный код? Часть вторая: связность 1




Недавно всплыло

5 В каких ситуациях текст лучше иллюстраций? 2 5 Мне трудно отказывать людям, и я не хочу ни с кем портить отношения 13