Автотесты «на пальцах»
Как тестировать модуль, использующий другой модуль
Интеграционные тесты
Автотесты «на пальцах»
Как тестировать модуль, использующий другой модуль
Интеграционные тесты
В прошлом совете я рассказывал о функциях‑запросах, функциях‑командах и о том, как проверять их: у запросов проверяем возвращаемое значение, у команд — последствия. Сегодня расскажу о тестировании модулей, активно использующих другие внешние модули.
Модули редко существуют изолированно друг от друга. Чаще всего они зависят от других модулей. Например, обращаются к ним за какими‑то данными:
«Корзина» спрашивает сумму у «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. Это был совет о веб‑разработке. Хотите знать всё о коде, тестах, фронтенд‑разработке, цеэсэсе, яваскрипте, рельсах и джейде? Присылайте вопросы.