Паттерны
October 29, 2024

AAA-паттерн в unit-тестировании на JavaScript

Паттерны программирования помогают разработчикам читать и понимать чужой код. AAA-паттерн в контексте unit-тестирования предлагает структурировать тесты на три секции:

  1. Arrange (настрой)
  2. Act (действуй)
  3. Assert (проверь)

Настрой

На этапе настройки подготавливаются все зависимости теста:

  • Объявляем аргументы для функций
  • Объявляем props для компонентов

Иногда настраивать ничего не требуется, это нормально.

Действуй

На этапе действия выполняется основная задача теста:

  • Рендерим 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-тестов, так как требует чтобы тесты проводили проверки, а не ходили по шагам.

Использовать паттерн — хорошая практика, так как разработчикам становиться проще читать и понимать чужие тесты.