In-source тестирование

In-source тестирование — подход, при котором тесты располагаются непосредственно рядом с исходным кодом: внутри того же файла или в непосредственной близости от него. В экосистеме Vite и Vitest такой формат получил широкое распространение благодаря высокой скорости запуска, поддержке ES-модулей и тесной интеграции с системой трансформации Vite.

Главная идея заключается в том, что тест становится частью модуля, а не отдельным артефактом проекта. Код и его проверка существуют вместе, синхронно развиваются и легче поддерживаются.

Типичная структура:

export function sum(a: number, b: number): number {
    return a + b;
}

if (import.meta.vitest) {
    const { describe, it, expect } = import.meta.vitest;

    describe('sum', () => {
        it('adds numbers', () => {
            expect(sum(2, 3)).toBe(5);
        });
    });
}

Vitest анализирует специальное свойство import.meta.vitest, благодаря чему тестовые блоки не попадают в production-сборку.


Причины появления in-source тестирования

Классическая организация тестов предполагает отдельные директории:

src/
tests/
__tests__/

Подобная схема остаётся актуальной, однако со временем появились проблемы:

  • разрыв между кодом и тестами;
  • сложность навигации;
  • дублирование импортов;
  • высокая стоимость поддержки небольших unit-тестов;
  • потеря контекста при рефакторинге.

In-source подход решает эти проблемы за счёт локализации тестовой логики.

Пример:

export function clamp(value: number, min: number, max: number): number {
    return Math.min(Math.max(value, min), max);
}

if (import.meta.vitest) {
    const { it, expect } = import.meta.vitest;

    it('limits value', () => {
        expect(clamp(20, 0, 10)).toBe(10);
    });
}

Тест всегда находится рядом с реализацией функции.


Роль import.meta.vitest

Vitest внедряет специальное свойство:

import.meta.vitest

Оно существует только во время тестового выполнения. В обычном runtime значение отсутствует.

Проверка:

if (import.meta.vitest) {

}

служит одновременно:

  • условием выполнения тестов;
  • сигналом для tree-shaking;
  • границей между production-кодом и тестовой средой.

Как работает исключение тестов из production-сборки

Во время production build Vite использует Rollup и систему tree-shaking.

Конструкция:

if (import.meta.vitest) {

}

считается недостижимой, поскольку import.meta.vitest не определён в production-режиме.

В результате:

  • тестовый код удаляется;
  • импорты тестов исключаются;
  • размер бандла не увеличивается.

Это позволяет безопасно хранить тесты прямо в production-модулях.


Активация in-source тестирования

В конфигурации Vitest необходимо включить параметр:

import { defineConfig } from 'vitest/config';

export default defineConfig({
    test: {
        includeSource: ['src/**/*.{js,ts}']
    }
});

Параметр includeSource сообщает Vitest:

  • анализировать обычные source-файлы;
  • искать внутри них тестовые блоки;
  • исполнять найденные тесты.

Без этой настройки тесты внутри исходников обнаружены не будут.


Отличие от обычных тестовых файлов

Классический подход

math.ts
math.test.ts

In-source подход

math.ts

с встроенными тестами.


Преимущества in-source тестирования

Улучшенная локальность кода

Тест и реализация находятся рядом:

export function square(x: number): number {
    return x * x;
}

if (import.meta.vitest) {
    const { test, expect } = import.meta.vitest;

    test('square', () => {
        expect(square(4)).toBe(16);
    });
}

Не требуется переключение между файлами.


Быстрое сопровождение

При изменении алгоритма тесты обновляются одновременно.

Риск забыть обновить тест снижается.


Удобство рефакторинга

IDE перемещает код вместе с тестами.

При переименовании функции:

calculateTotal

тест обновляется автоматически внутри того же файла.


Минимизация лишних импортов

Отсутствуют повторные импорты:

import { calculate } from './calculate';

Функция уже находится в области видимости.


Подходит для utility-функций

Особенно эффективно для:

  • математических функций;
  • преобразований;
  • parser-функций;
  • validation-логики;
  • helper-модулей;
  • composable-функций.

Недостатки подхода

Увеличение размера source-файлов

Крупный модуль может превратиться в смесь:

  • production-кода;
  • моков;
  • тестов;
  • вспомогательных данных.

Сложность с интеграционными тестами

In-source плохо подходит для:

  • e2e;
  • browser testing;
  • API integration;
  • UI interaction testing.

Перегрузка бизнес-кода

Некоторые команды считают:

if (import.meta.vitest)

визуальным шумом.


Проблемы со сложными fixture

Большие mock-данные внутри source-файла ухудшают читаемость.


Когда in-source тестирование особенно полезно

Utility-модули

export function kebabCase(value: string): string {
    return value
        .trim()
        .toLowerCase()
        .replace(/\s+/g, '-');
}

Алгоритмы

export function binarySearch() {

}

Валидация

export function isEmail(value: string): boolean {

}

Pure functions

Функции без побочных эффектов идеально подходят для in-source тестов.


Когда лучше использовать отдельные тестовые файлы

UI-компоненты

Например:

  • Vue;
  • React;
  • Svelte.

Интеграционные тесты

Когда требуется:

  • подготовка окружения;
  • mock API;
  • сложные сценарии;
  • lifecycle setup.

Большие тестовые наборы

Если тест занимает больше места, чем реализация — лучше вынести его отдельно.


Использование describe

export function multiply(a: number, b: number): number {
    return a * b;
}

if (import.meta.vitest) {
    const { describe, it, expect } = import.meta.vitest;

    describe('multiply', () => {
        it('multiplies numbers', () => {
            expect(multiply(3, 4)).toBe(12);
        });

        it('handles zero', () => {
            expect(multiply(0, 10)).toBe(0);
        });
    });
}

Использование beforeEach

class Counter {
    value = 0;

    increment() {
        this.value++;
    }
}

if (import.meta.vitest) {
    const {
        describe,
        it,
        expect,
        beforeEach
    } = import.meta.vitest;

    let counter: Counter;

    beforeEach(() => {
        counter = new Counter();
    });

    describe('Counter', () => {
        it('increments', () => {
            counter.increment();

            expect(counter.value).toBe(1);
        });
    });
}

Использование моков

export async function loadUser(fetcher = fetch) {
    const response = await fetcher('/api/user');

    return response.json();
}

if (import.meta.vitest) {
    const { it, expect, vi } = import.meta.vitest;

    it('loads user', async () => {
        const fetcher = vi.fn().mockResolvedValue({
            json: () => Promise.resolve({
                id: 1
            })
        });

        const user = await loadUser(fetcher);

        expect(user.id).toBe(1);
    });
}

In-source тестирование и TypeScript

Vitest хорошо интегрирован с TypeScript.

Пример типизированной функции:

type User = {
    id: number;
    name: string;
};

export function createUser(name: string): User {
    return {
        id: 1,
        name
    };
}

if (import.meta.vitest) {
    const { it, expect } = import.meta.vitest;

    it('creates user', () => {
        const user = createUser('Alex');

        expect(user.name).toBe('Alex');
    });
}

Глобальный API vs import.meta.vitest

Глобальный API

describe()
it()
expect()

In-source API

const { describe, it, expect } = import.meta.vitest;

Второй вариант предпочтительнее для in-source тестирования, поскольку:

  • отсутствуют глобальные зависимости;
  • легче tree-shaking;
  • меньше магии;
  • лучше читаемость зависимостей.

Изоляция тестов

Каждый тест должен быть независимым.

Плохой пример:

let value = 0;

it('a', () => {
    value++;
});

it('b', () => {
    expect(value).toBe(0);
});

Корректный вариант:

beforeEach(() => {
    value = 0;
});

Покрытие кода

In-source тесты участвуют в code coverage так же, как и обычные.

Конфигурация:

export default defineConfig({
    test: {
        coverage: {
            reporter: ['text', 'html']
        }
    }
});

Запуск:

vitest run --coverage

Совместимость с HMR

Одной из сильных сторон Vitest является интеграция с dev server Vite.

При изменении source-файла:

  • модуль пересобирается;
  • тесты перезапускаются;
  • результаты обновляются почти мгновенно.

Это особенно эффективно для небольших utility-функций.


Использование snapshot-тестов

export function getUser() {
    return {
        id: 1,
        role: 'admin'
    };
}

if (import.meta.vitest) {
    const { it, expect } = import.meta.vitest;

    it('matches snapshot', () => {
        expect(getUser()).toMatchSnapshot();
    });
}

Организация больших in-source тестов

Даже при использовании встроенных тестов рекомендуется соблюдать структуру:

/*
|--------------------------------------------------------------------------
| Public API
|--------------------------------------------------------------------------
*/

export function a() {

}

/*
|--------------------------------------------------------------------------
| Private helpers
|--------------------------------------------------------------------------
*/

function helper() {

}

/*
|--------------------------------------------------------------------------
| Tests
|--------------------------------------------------------------------------
*/

if (import.meta.vitest) {

}

Подобное разделение сохраняет читаемость файла.


Комбинирование с отдельными тестами

Подходы можно комбинировать.

Например:

  • utility-функции тестируются in-source;
  • интеграционные тесты располагаются отдельно;
  • e2e остаются в dedicated-директории.

Это наиболее распространённая стратегия в крупных проектах.


Работа с приватными функциями

In-source тестирование позволяет тестировать внутренние функции без их экспорта.

Пример:

function normalize(value: string): string {
    return value.trim().toLowerCase();
}

export function compare(a: string, b: string): boolean {
    return normalize(a) === normalize(b);
}

if (import.meta.vitest) {
    const { it, expect } = import.meta.vitest;

    it('normalizes strings', () => {
        expect(normalize(' TEST ')).toBe('test');
    });
}

Это одно из ключевых преимуществ подхода.


Влияние на архитектуру модулей

In-source тестирование стимулирует:

  • создание маленьких модулей;
  • чистые функции;
  • уменьшение связанности;
  • отказ от скрытых зависимостей.

Модули становятся проще для локального тестирования.


Производительность

Vitest использует:

  • ESBuild;
  • Vite transform pipeline;
  • кеширование модулей;
  • ESM runtime.

Благодаря этому даже большое количество in-source тестов выполняется очень быстро.


Практические рекомендации

Не смешивать большие integration tests с source-кодом


Хранить только локальные unit-тесты


Избегать огромных mock-структур


Использовать describe для группировки


Поддерживать компактность тестов


Не тестировать внутренние детали без необходимости


Типичная структура проекта

src/
├── utils/
│   ├── math.ts
│   ├── strings.ts
│   └── date.ts
│
├── services/
│   ├── api.ts
│   └── auth.ts
│
tests/
├── integration/
├── e2e/
└── browser/

Где:

  • utility-модули используют in-source testing;
  • интеграционные тесты вынесены отдельно.

Пример полноценного in-source модуля

export function factorial(n: number): number {
    if (n <= 1) {
        return 1;
    }

    return n * factorial(n - 1);
}

if (import.meta.vitest) {
    const {
        describe,
        it,
        expect
    } = import.meta.vitest;

    describe('factorial', () => {
        it('calculates factorial', () => {
            expect(factorial(5)).toBe(120);
        });

        it('handles zero', () => {
            expect(factorial(0)).toBe(1);
        });

        it('handles one', () => {
            expect(factorial(1)).toBe(1);
        });
    });
}

Такой модуль:

  • полностью самодостаточен;
  • содержит реализацию и проверки;
  • легко переносится;
  • прост в сопровождении;
  • быстро выполняется в Vitest.