AAA-паттерн в unit-тестировании на JavaScript
Паттерны программирования помогают разработчикам читать и понимать чужой код. AAA-паттерн в контексте unit-тестирования предлагает структурировать тесты на три секции:
Настрой
На этапе настройки подготавливаются все зависимости теста:
Иногда настраивать ничего не требуется, это нормально.
Действуй
На этапе действия выполняется основная задача теста:
- Рендерим React-компонент
- Получаем DOM-элементы
- Вызываем функции с аргументами
- Изменяем состояние в React-хуках
- Инициируем действие пользователя (клик, фокус и т.д.)
Проверь
На этапе проверки сверяем результаты выполнения сценариев:
Примеры с тестами
Чистые функции
Проверяем по AAA-паттерну результат работы чистой функции:
describe('dateFormat', () => {
it('дата должна соотвествовать формату DD-MM-YYYY', () => {
// Настрой
const inputDate = '07.12.2012'
const outputFormat = 'DD-MM-YYYY'
// Действуй
const date = dateFormat(inputDate, outputFormat)
// Проверь
expect(date).toBe('07-12-2012')
})
}Изменение state в React-хуке
Проверяем по AAA-паттерну бизнес-логику, вынесенную в отдельный хук:
function useAnalytics() {
const [events, setEventCount] = useState(0)
const sendEvent = debounce(async (event) => {
await ym(event)
setEventCount((x) => x + 1)
}, 300)
return { sendEvent, events }
}import { renderHook, waitFor } from "@testing-library/react"
describe('useAnalytics', () => {
it('только 1 событие отправлено после 2 кликов подряд', async () => {
// Действуй
const { sendEvent, events } = renderHook(useAnalytics)
waitFor(() => {
await sendEvent('click')
await sendEvent('click')
})
// Проверь
expect(events).toBe(1)
})
}Изменение state в Redux-редюсере
Проверяем по AAA-паттерну бизнес-логику вынесенную в отдельный редюсер, при помощи доступных методов:
const ratesReducer = createSlice({
name: 'rates',
initialState: {
all: [],
best: [],
},
reducers: {
set: (state, action) => {
state.all = action.payload
}
filter: (state, action) => {
const { all, best } = state
const { value } = action.payload
best = all.filter(rate => rate.value >= value)
}
}
})describe('ratesReducer', () => {
it('возвращает ставки при фильтрации по значению', () => {
// Настрой
const initials = {
all: [
{ name: 'Накопительный', value: 11 },
{ name: 'Среднесрочный', value: 14 },
{ name: 'Долгосрочный', value: 20 },
],
best: []
}
// Действуй
const { reducer, filter } = rates
const bestRates = reducer(initials, filter({ value: 12 })
// Проверь
expect(bestRates).toEqual([
{ name: 'Среднесрочный', value: 14 },
{ name: 'Долгосрочный', value: 18 },
])
})
})Изменение props в React-компоненте
Проверяем по AAA-паттерну рендеринг компонента с различными сочетаниями props:
import { render } from "@testing-library/react"
describe('Button', () => {
it('отображает загрузку (без текста) для состояния loading', () => {
// Настрой
const props = {
isLoading: true
}
// Действуй
const component = render(<Button {...props}>Скачать</Button>)
const button = component.getByTestId('button')
// Проверь
expect(button).toHaveClass('.loading')
expect(button).not.toHaveTextContent("Скачать")
})
})Тестируем внутреннее изменение state в React-компоненте по AAA-паттерну:
import { render, fireEvent } from "@testing-library/react"
describe("Toggle", () => {
it("включён после клика на Toggle в дефолтном состоянии", () => {
// Действуй
const component = render(<Toggle />)
const toggle = component.getByTestId('.toggle')
waitFor(() => {
fireEvent.click(toggle)
})
// Проверь
expect(toggle).toBeChecked()
})
})Название теста
Имя теста так же можно создавать при помощи AAA-паттерна, только в обратную сторону (на примере тестов выше):
- (dateFormat) date must be exact to DD-MM-YYYY format
- (useAnalytics) sent only one event after double call by sendEvent
- (ratesReducer) returns rates with filter value
- (button) show loading styles and no text for loading state
- (toggle) checked after click to toggle with default state
Заключение
Паттерн хорош собой, так как при создании теста заставляет концентрироваться на независимых этапах, которые при повторении можно легко вынести и переиспользовать.
Так же паттерн создает условия, при которых код с бизнес-логикой нужно выносить в отдельные чистые функции, что в свою очередь считается другим хорошим паттерном.
Паттерн подходит только для unit-тестов, так как требует чтобы тесты проводили проверки, а не ходили по шагам.
Использовать паттерн — хорошая практика, так как разработчикам становиться проще читать и понимать чужие тесты.