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

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

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

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

«Корзина» спрашивает сумму у «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. Это был совет о веб‑разработке. Хотите знать всё о коде, тестах, фронтенд‑разработке, цеэсэсе, яваскрипте, рельсах и джейде? Присылайте вопросы.

Веб‑разработка
Отправить
Поделиться
Запинить

Рекомендуем другие советы