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-тестов, так как требует чтобы тесты проводили проверки, а не ходили по шагам.
Использовать паттерн — хорошая практика, так как разработчикам становиться проще читать и понимать чужие тесты.