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-сборку.
Классическая организация тестов предполагает отдельные директории:
src/
tests/
__tests__/
Подобная схема остаётся актуальной, однако со временем появились проблемы:
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.vitestVitest внедряет специальное свойство:
import.meta.vitest
Оно существует только во время тестового выполнения. В обычном runtime значение отсутствует.
Проверка:
if (import.meta.vitest) {
}
служит одновременно:
Во время production build Vite использует Rollup и систему tree-shaking.
Конструкция:
if (import.meta.vitest) {
}
считается недостижимой, поскольку import.meta.vitest не
определён в production-режиме.
В результате:
Это позволяет безопасно хранить тесты прямо в production-модулях.
В конфигурации Vitest необходимо включить параметр:
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
includeSource: ['src/**/*.{js,ts}']
}
});
Параметр includeSource сообщает Vitest:
Без этой настройки тесты внутри исходников обнаружены не будут.
math.ts
math.test.ts
math.ts
с встроенными тестами.
Тест и реализация находятся рядом:
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';
Функция уже находится в области видимости.
Особенно эффективно для:
Крупный модуль может превратиться в смесь:
In-source плохо подходит для:
Некоторые команды считают:
if (import.meta.vitest)
визуальным шумом.
Большие mock-данные внутри source-файла ухудшают читаемость.
export function kebabCase(value: string): string {
return value
.trim()
.toLowerCase()
.replace(/\s+/g, '-');
}
export function binarySearch() {
}
export function isEmail(value: string): boolean {
}
Функции без побочных эффектов идеально подходят для in-source тестов.
Например:
Когда требуется:
Если тест занимает больше места, чем реализация — лучше вынести его отдельно.
describeexport 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);
});
});
}
beforeEachclass 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);
});
}
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');
});
}
import.meta.vitestdescribe()
it()
expect()
const { describe, it, expect } = import.meta.vitest;
Второй вариант предпочтительнее для in-source тестирования, поскольку:
Каждый тест должен быть независимым.
Плохой пример:
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
Одной из сильных сторон Vitest является интеграция с dev server Vite.
При изменении source-файла:
Это особенно эффективно для небольших utility-функций.
export function getUser() {
return {
id: 1,
role: 'admin'
};
}
if (import.meta.vitest) {
const { it, expect } = import.meta.vitest;
it('matches snapshot', () => {
expect(getUser()).toMatchSnapshot();
});
}
Даже при использовании встроенных тестов рекомендуется соблюдать структуру:
/*
|--------------------------------------------------------------------------
| Public API
|--------------------------------------------------------------------------
*/
export function a() {
}
/*
|--------------------------------------------------------------------------
| Private helpers
|--------------------------------------------------------------------------
*/
function helper() {
}
/*
|--------------------------------------------------------------------------
| Tests
|--------------------------------------------------------------------------
*/
if (import.meta.vitest) {
}
Подобное разделение сохраняет читаемость файла.
Подходы можно комбинировать.
Например:
Это наиболее распространённая стратегия в крупных проектах.
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 использует:
Благодаря этому даже большое количество in-source тестов выполняется очень быстро.
describe для группировкиsrc/
├── utils/
│ ├── math.ts
│ ├── strings.ts
│ └── date.ts
│
├── services/
│ ├── api.ts
│ └── auth.ts
│
tests/
├── integration/
├── e2e/
└── browser/
Где:
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);
});
});
}
Такой модуль: