lenec ru

← все посты

Async/await в TypeScript: практические паттерны тестирования

2

Асинхронный код в TypeScript стал стандартом современной разработки, но его тестирование требует особого подхода. В этой статье разберём практические паттерны тестирования async/await конструкций, которые помогут писать надёжные тесты для асинхронной логики.

Базовое тестирование async функций

Современные тестовые фреймворки (Jest, Vitest, Mocha) поддерживают async/await из коробки. Главное правило — тестовая функция должна быть async, если внутри используется await:

import { describe, it, expect } from 'vitest';

async function fetchUserData(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  if (!response.ok) throw new Error('User not found');
  return response.json();
}

describe('fetchUserData', () => {
  it('должна вернуть данные пользователя', async () => {
    const user = await fetchUserData(1);
    expect(user.id).toBe(1);
    expect(user.name).toBeDefined();
  });
});

Если забыть await, тест завершится до выполнения асинхронной операции, и проверки не сработают. TypeScript поможет отловить такие ошибки через типизацию Promise.

Мокирование асинхронных зависимостей

Для изоляции тестов нужно мокировать внешние вызовы. В Jest/Vitest это делается через vi.fn() или jest.fn():

import { vi } from 'vitest';

const mockFetch = vi.fn();
global.fetch = mockFetch;

describe('fetchUserData с моками', () => {
  it('обрабатывает успешный ответ', async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ id: 1, name: 'Alice' })
    });

    const user = await fetchUserData(1);
    expect(user.name).toBe('Alice');
    expect(mockFetch).toHaveBeenCalledWith('/api/users/1');
  });

  it('выбрасывает ошибку при 404', async () => {
    mockFetch.mockResolvedValueOnce({ ok: false });

    await expect(fetchUserData(999))
      .rejects.toThrow('User not found');
  });
});

Метод mockResolvedValueOnce возвращает resolved Promise, а mockRejectedValueOnce — rejected. Это позволяет тестировать как успешные сценарии, так и обработку ошибок.

Тестирование обработки ошибок

Async/await упрощает обработку ошибок через try/catch, но в тестах нужно проверять оба пути выполнения:

async function processData(data: string): Promise<Result> {
  try {
    const parsed = JSON.parse(data);
    const validated = await validateSchema(parsed);
    return { success: true, data: validated };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error.message : 'Unknown error' 
    };
  }
}

describe('processData error handling', () => {
  it('возвращает ошибку при невалидном JSON', async () => {
    const result = await processData('invalid json');
    expect(result.success).toBe(false);
    expect(result.error).toContain('JSON');
  });

  it('возвращает ошибку при провале валидации', async () => {
    vi.mocked(validateSchema).mockRejectedValueOnce(
      new Error('Schema validation failed')
    );

    const result = await processData('{"valid": "json"}');
    expect(result.success).toBe(false);
    expect(result.error).toBe('Schema validation failed');
  });
});

Параллельное выполнение и Promise.all

При тестировании параллельных операций важно проверить, что они действительно выполняются одновременно, а не последовательно:

async function fetchMultipleUsers(ids: number[]): Promise<User[]> {
  const promises = ids.map(id => fetchUserData(id));
  return Promise.all(promises);
}

describe('fetchMultipleUsers', () => {
  it('выполняет запросы параллельно', async () => {
    const startTime = Date.now();
    
    mockFetch.mockImplementation(async (url: string) => {
      await new Promise(resolve => setTimeout(resolve, 100));
      const id = parseInt(url.split('/').pop()!);
      return {
        ok: true,
        json: async () => ({ id, name: `User${id}` })
      };
    });

    const users = await fetchMultipleUsers([1, 2, 3]);
    const duration = Date.now() - startTime;

    expect(users).toHaveLength(3);
    expect(duration).toBeLessThan(200); // не 300+, значит параллельно
  });

  it('обрабатывает частичные ошибки', async () => {
    mockFetch
      .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 1 }) })
      .mockResolvedValueOnce({ ok: false })
      .mockResolvedValueOnce({ ok: true, json: async () => ({ id: 3 }) });

    await expect(fetchMultipleUsers([1, 2, 3]))
      .rejects.toThrow();
  });
});

Подводные камни

  • Забытый await — тест завершится раньше времени. Включите правило ESLint @typescript-eslint/no-floating-promises.
  • Неочищенные моки — используйте beforeEach(() => vi.clearAllMocks()) для изоляции тестов.
  • Race conditions — при тестировании конкурентного кода добавляйте искусственные задержки через vi.useFakeTimers().
  • Unhandled rejections — всегда оборачивайте ожидаемые ошибки в expect().rejects или try/catch.

Вывод

Тестирование async/await в TypeScript требует внимания к деталям: правильное использование await в тестах, изоляция через моки, проверка обработки ошибок и корректное тестирование параллельных операций. Следуя этим паттернам, вы получите надёжные тесты, которые ловят баги до продакшена и документируют поведение асинхронного кода.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.