Vitest — современный тест-раннер с нативной поддержкой TypeScript:
npm install -D vitest @vitest/ui// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.{test,spec}.ts'],
},
})// utils/math.ts
export function add(a: number, b: number): number {
return a + b
}
// utils/math.test.ts
import { describe, it, expect } from 'vitest'
import { add } from './math'
describe('add', () => {
it('складывает два числа', () => {
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
})
it('принимает только числа (TypeScript защищает)', () => {
// @ts-expect-error — намеренно неправильный тип
expect(() => add('2', 3)).not.toThrow()
})
})function first<T>(arr: T[]): T | undefined {
return arr[0]
}
it('возвращает первый элемент', () => {
expect(first([1, 2, 3])).toBe(1)
expect(first(['a', 'b'])).toBe('a')
expect(first([])).toBeUndefined()
})Vitest предоставляет expectTypeOf для проверки типов во время тестов:
import { expectTypeOf } from 'vitest'
it('проверяет типы', () => {
expectTypeOf(add).toBeFunction()
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>()
expectTypeOf(add).returns.toBeNumber()
expectTypeOf(first<string>).returns.toEqualTypeOf<string | undefined>()
})import { vi, Mock } from 'vitest'
interface UserService {
getUser(id: number): Promise<User>
createUser(data: CreateUserData): Promise<User>
}
// vi.fn() создаёт мок-функцию с правильными типами:
const mockGetUser = vi.fn<[number], Promise<User>>()
mockGetUser.mockResolvedValue({ id: 1, name: 'Алексей' })
// Или через vi.mocked():
const mockService = {
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Алексей' }),
createUser: vi.fn().mockResolvedValue({ id: 2, name: 'Мария' }),
} satisfies Record<keyof UserService, Mock>async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
it('получает пользователя', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Алексей' }),
})
const user = await fetchUser(1)
expect(user.name).toBe('Алексей')
expect(fetch).toHaveBeenCalledWith('/api/users/1')
})
it('выбрасывает ошибку при 404', async () => {
global.fetch = vi.fn().mockResolvedValue({ ok: false })
await expect(fetchUser(99)).rejects.toThrow('User not found')
})class Stack<T> {
private items: T[] = []
push(item: T): void { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
peek(): T | undefined { return this.items[this.items.length - 1] }
get size(): number { return this.items.length }
}
describe('Stack<T>', () => {
it('работает с числами', () => {
const stack = new Stack<number>()
stack.push(1); stack.push(2)
expect(stack.pop()).toBe(2)
expect(stack.size).toBe(1)
})
it('работает со строками', () => {
const stack = new Stack<string>()
stack.push('hello')
expect(stack.peek()).toBe('hello')
expect(stack.size).toBe(1)
})
})export default {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\.tsx?$': ['ts-jest', { useESM: true }],
},
}Мини тест-раннер с describe/it/expect/mock — реализация с нуля, демонстрирует как работает Vitest/Jest внутри
// Реализуем минимальный тест-раннер — понимаем как устроены Vitest/Jest.
// В TypeScript тесты имеют строгую типизацию через expectTypeOf.
// --- Мини тест-раннер ---
let passed = 0
let failed = 0
let currentSuite = 'root'
function describe(name, fn) {
const prev = currentSuite
currentSuite = name
console.log(`\n ${name}`)
fn()
currentSuite = prev
}
function it(name, fn) {
try {
const result = fn()
if (result instanceof Promise) {
return result.then(() => {
passed++
console.log(` ✓ ${name}`)
}).catch(err => {
failed++
console.log(` ✗ ${name}: ${err.message}`)
})
}
passed++
console.log(` ✓ ${name}`)
} catch (err) {
failed++
console.log(` ✗ ${name}: ${err.message}`)
}
}
const test = it
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}
},
toEqual(expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}
},
toBeUndefined() {
if (actual !== undefined) throw new Error(`Expected undefined, got ${actual}`)
},
toBeNull() {
if (actual !== null) throw new Error(`Expected null, got ${actual}`)
},
toBeTruthy() {
if (!actual) throw new Error(`Expected truthy, got ${actual}`)
},
toBeFalsy() {
if (actual) throw new Error(`Expected falsy, got ${actual}`)
},
toContain(item) {
if (Array.isArray(actual)) {
if (!actual.includes(item)) throw new Error(`Expected array to contain ${item}`)
} else if (typeof actual === 'string') {
if (!actual.includes(item)) throw new Error(`Expected string to contain ${item}`)
}
},
toHaveLength(len) {
if (actual.length !== len) throw new Error(`Expected length ${len}, got ${actual.length}`)
},
toThrow(message) {
if (typeof actual !== 'function') throw new Error('toThrow requires a function')
try { actual(); throw new Error('Expected to throw') }
catch (e) {
if (e.message === 'Expected to throw') throw e
if (message && !e.message.includes(message)) {
throw new Error(`Expected error "${message}", got "${e.message}"`)
}
}
},
resolves: {
toBe: (expected) => actual.then(v => expect(v).toBe(expected)),
},
rejects: {
toThrow: (msg) => actual.catch(e => {
if (msg && !e.message.includes(msg)) throw new Error(`Wrong error: ${e.message}`)
}).then(() => {}, () => { throw new Error('Expected rejection') })
},
not: {
toBe(expected) {
if (actual === expected) throw new Error(`Expected NOT ${JSON.stringify(expected)}`)
},
toBeUndefined() {
if (actual === undefined) throw new Error('Expected to not be undefined')
},
}
}
}
// --- vi.fn() мок ---
function vi_fn(implementation = () => undefined) {
const calls = []
const mockImpl = { current: implementation }
function mockFn(...args) {
calls.push(args)
return mockImpl.current(...args)
}
mockFn.mock = { calls }
mockFn.mockReturnValue = (val) => { mockImpl.current = () => val; return mockFn }
mockFn.mockResolvedValue = (val) => { mockImpl.current = () => Promise.resolve(val); return mockFn }
mockFn.mockRejectedValue = (err) => { mockImpl.current = () => Promise.reject(err); return mockFn }
mockFn.mockImplementation = (fn) => { mockImpl.current = fn; return mockFn }
return mockFn
}
// --- Тестируемый код (как TypeScript) ---
// TS: function add(a: number, b: number): number
function add(a, b) { return a + b }
// TS: function first<T>(arr: T[]): T | undefined
function first(arr) { return arr[0] }
// TS: class Stack<T>
class Stack {
constructor() { this.items = [] }
push(item) { this.items.push(item) }
pop() { return this.items.pop() }
peek() { return this.items[this.items.length - 1] }
get size() { return this.items.length }
}
// TS: async function fetchUser(id: number): Promise<User>
async function fetchUser(id, fetchFn) {
const response = await fetchFn(`/api/users/${id}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
// --- ТЕСТЫ ---
console.log('Running tests...')
describe('add()', () => {
it('складывает два числа', () => {
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
expect(add(0, 0)).toBe(0)
})
it('работает с отрицательными числами', () => {
expect(add(-5, -3)).toBe(-8)
})
})
describe('first<T>()', () => {
it('возвращает первый элемент', () => {
expect(first([1, 2, 3])).toBe(1)
expect(first(['a', 'b'])).toBe('a')
})
it('возвращает undefined для пустого массива', () => {
expect(first([])).toBeUndefined()
})
})
describe('Stack<T>', () => {
it('push и pop работают корректно', () => {
const stack = new Stack()
stack.push(1); stack.push(2); stack.push(3)
expect(stack.size).toBe(3)
expect(stack.pop()).toBe(3)
expect(stack.size).toBe(2)
})
it('peek не удаляет элемент', () => {
const stack = new Stack()
stack.push('hello')
expect(stack.peek()).toBe('hello')
expect(stack.size).toBe(1)
})
it('pop из пустого стека возвращает undefined', () => {
const stack = new Stack()
expect(stack.pop()).toBeUndefined()
})
})
describe('fetchUser() с моками', () => {
it('возвращает пользователя', async () => {
const mockFetch = vi_fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Алексей' }),
})
const user = await fetchUser(1, mockFetch)
expect(user.name).toBe('Алексей')
expect(mockFetch.mock.calls[0][0]).toBe('/api/users/1')
})
it('выбрасывает ошибку при 404', async () => {
const mockFetch = vi_fn().mockResolvedValue({ ok: false })
try {
await fetchUser(99, mockFetch)
throw new Error('Должно было выбросить ошибку')
} catch (e) {
expect(e.message).toBe('User not found')
passed++
console.log(' ✓ выбрасывает ошибку при 404')
}
})
})
// Итоги (асинхронные тесты завершатся после)
setTimeout(() => {
console.log(`\n Results: ${passed} passed, ${failed} failed`)
}, 100)Vitest — современный тест-раннер с нативной поддержкой TypeScript:
npm install -D vitest @vitest/ui// vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true,
environment: 'node',
include: ['src/**/*.{test,spec}.ts'],
},
})// utils/math.ts
export function add(a: number, b: number): number {
return a + b
}
// utils/math.test.ts
import { describe, it, expect } from 'vitest'
import { add } from './math'
describe('add', () => {
it('складывает два числа', () => {
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
})
it('принимает только числа (TypeScript защищает)', () => {
// @ts-expect-error — намеренно неправильный тип
expect(() => add('2', 3)).not.toThrow()
})
})function first<T>(arr: T[]): T | undefined {
return arr[0]
}
it('возвращает первый элемент', () => {
expect(first([1, 2, 3])).toBe(1)
expect(first(['a', 'b'])).toBe('a')
expect(first([])).toBeUndefined()
})Vitest предоставляет expectTypeOf для проверки типов во время тестов:
import { expectTypeOf } from 'vitest'
it('проверяет типы', () => {
expectTypeOf(add).toBeFunction()
expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>()
expectTypeOf(add).returns.toBeNumber()
expectTypeOf(first<string>).returns.toEqualTypeOf<string | undefined>()
})import { vi, Mock } from 'vitest'
interface UserService {
getUser(id: number): Promise<User>
createUser(data: CreateUserData): Promise<User>
}
// vi.fn() создаёт мок-функцию с правильными типами:
const mockGetUser = vi.fn<[number], Promise<User>>()
mockGetUser.mockResolvedValue({ id: 1, name: 'Алексей' })
// Или через vi.mocked():
const mockService = {
getUser: vi.fn().mockResolvedValue({ id: 1, name: 'Алексей' }),
createUser: vi.fn().mockResolvedValue({ id: 2, name: 'Мария' }),
} satisfies Record<keyof UserService, Mock>async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
it('получает пользователя', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Алексей' }),
})
const user = await fetchUser(1)
expect(user.name).toBe('Алексей')
expect(fetch).toHaveBeenCalledWith('/api/users/1')
})
it('выбрасывает ошибку при 404', async () => {
global.fetch = vi.fn().mockResolvedValue({ ok: false })
await expect(fetchUser(99)).rejects.toThrow('User not found')
})class Stack<T> {
private items: T[] = []
push(item: T): void { this.items.push(item) }
pop(): T | undefined { return this.items.pop() }
peek(): T | undefined { return this.items[this.items.length - 1] }
get size(): number { return this.items.length }
}
describe('Stack<T>', () => {
it('работает с числами', () => {
const stack = new Stack<number>()
stack.push(1); stack.push(2)
expect(stack.pop()).toBe(2)
expect(stack.size).toBe(1)
})
it('работает со строками', () => {
const stack = new Stack<string>()
stack.push('hello')
expect(stack.peek()).toBe('hello')
expect(stack.size).toBe(1)
})
})export default {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\.tsx?$': ['ts-jest', { useESM: true }],
},
}Мини тест-раннер с describe/it/expect/mock — реализация с нуля, демонстрирует как работает Vitest/Jest внутри
// Реализуем минимальный тест-раннер — понимаем как устроены Vitest/Jest.
// В TypeScript тесты имеют строгую типизацию через expectTypeOf.
// --- Мини тест-раннер ---
let passed = 0
let failed = 0
let currentSuite = 'root'
function describe(name, fn) {
const prev = currentSuite
currentSuite = name
console.log(`\n ${name}`)
fn()
currentSuite = prev
}
function it(name, fn) {
try {
const result = fn()
if (result instanceof Promise) {
return result.then(() => {
passed++
console.log(` ✓ ${name}`)
}).catch(err => {
failed++
console.log(` ✗ ${name}: ${err.message}`)
})
}
passed++
console.log(` ✓ ${name}`)
} catch (err) {
failed++
console.log(` ✗ ${name}: ${err.message}`)
}
}
const test = it
function expect(actual) {
return {
toBe(expected) {
if (actual !== expected) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}
},
toEqual(expected) {
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`Expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`)
}
},
toBeUndefined() {
if (actual !== undefined) throw new Error(`Expected undefined, got ${actual}`)
},
toBeNull() {
if (actual !== null) throw new Error(`Expected null, got ${actual}`)
},
toBeTruthy() {
if (!actual) throw new Error(`Expected truthy, got ${actual}`)
},
toBeFalsy() {
if (actual) throw new Error(`Expected falsy, got ${actual}`)
},
toContain(item) {
if (Array.isArray(actual)) {
if (!actual.includes(item)) throw new Error(`Expected array to contain ${item}`)
} else if (typeof actual === 'string') {
if (!actual.includes(item)) throw new Error(`Expected string to contain ${item}`)
}
},
toHaveLength(len) {
if (actual.length !== len) throw new Error(`Expected length ${len}, got ${actual.length}`)
},
toThrow(message) {
if (typeof actual !== 'function') throw new Error('toThrow requires a function')
try { actual(); throw new Error('Expected to throw') }
catch (e) {
if (e.message === 'Expected to throw') throw e
if (message && !e.message.includes(message)) {
throw new Error(`Expected error "${message}", got "${e.message}"`)
}
}
},
resolves: {
toBe: (expected) => actual.then(v => expect(v).toBe(expected)),
},
rejects: {
toThrow: (msg) => actual.catch(e => {
if (msg && !e.message.includes(msg)) throw new Error(`Wrong error: ${e.message}`)
}).then(() => {}, () => { throw new Error('Expected rejection') })
},
not: {
toBe(expected) {
if (actual === expected) throw new Error(`Expected NOT ${JSON.stringify(expected)}`)
},
toBeUndefined() {
if (actual === undefined) throw new Error('Expected to not be undefined')
},
}
}
}
// --- vi.fn() мок ---
function vi_fn(implementation = () => undefined) {
const calls = []
const mockImpl = { current: implementation }
function mockFn(...args) {
calls.push(args)
return mockImpl.current(...args)
}
mockFn.mock = { calls }
mockFn.mockReturnValue = (val) => { mockImpl.current = () => val; return mockFn }
mockFn.mockResolvedValue = (val) => { mockImpl.current = () => Promise.resolve(val); return mockFn }
mockFn.mockRejectedValue = (err) => { mockImpl.current = () => Promise.reject(err); return mockFn }
mockFn.mockImplementation = (fn) => { mockImpl.current = fn; return mockFn }
return mockFn
}
// --- Тестируемый код (как TypeScript) ---
// TS: function add(a: number, b: number): number
function add(a, b) { return a + b }
// TS: function first<T>(arr: T[]): T | undefined
function first(arr) { return arr[0] }
// TS: class Stack<T>
class Stack {
constructor() { this.items = [] }
push(item) { this.items.push(item) }
pop() { return this.items.pop() }
peek() { return this.items[this.items.length - 1] }
get size() { return this.items.length }
}
// TS: async function fetchUser(id: number): Promise<User>
async function fetchUser(id, fetchFn) {
const response = await fetchFn(`/api/users/${id}`)
if (!response.ok) throw new Error('User not found')
return response.json()
}
// --- ТЕСТЫ ---
console.log('Running tests...')
describe('add()', () => {
it('складывает два числа', () => {
expect(add(2, 3)).toBe(5)
expect(add(-1, 1)).toBe(0)
expect(add(0, 0)).toBe(0)
})
it('работает с отрицательными числами', () => {
expect(add(-5, -3)).toBe(-8)
})
})
describe('first<T>()', () => {
it('возвращает первый элемент', () => {
expect(first([1, 2, 3])).toBe(1)
expect(first(['a', 'b'])).toBe('a')
})
it('возвращает undefined для пустого массива', () => {
expect(first([])).toBeUndefined()
})
})
describe('Stack<T>', () => {
it('push и pop работают корректно', () => {
const stack = new Stack()
stack.push(1); stack.push(2); stack.push(3)
expect(stack.size).toBe(3)
expect(stack.pop()).toBe(3)
expect(stack.size).toBe(2)
})
it('peek не удаляет элемент', () => {
const stack = new Stack()
stack.push('hello')
expect(stack.peek()).toBe('hello')
expect(stack.size).toBe(1)
})
it('pop из пустого стека возвращает undefined', () => {
const stack = new Stack()
expect(stack.pop()).toBeUndefined()
})
})
describe('fetchUser() с моками', () => {
it('возвращает пользователя', async () => {
const mockFetch = vi_fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ id: 1, name: 'Алексей' }),
})
const user = await fetchUser(1, mockFetch)
expect(user.name).toBe('Алексей')
expect(mockFetch.mock.calls[0][0]).toBe('/api/users/1')
})
it('выбрасывает ошибку при 404', async () => {
const mockFetch = vi_fn().mockResolvedValue({ ok: false })
try {
await fetchUser(99, mockFetch)
throw new Error('Должно было выбросить ошибку')
} catch (e) {
expect(e.message).toBe('User not found')
passed++
console.log(' ✓ выбрасывает ошибку при 404')
}
})
})
// Итоги (асинхронные тесты завершатся после)
setTimeout(() => {
console.log(`\n Results: ${passed} passed, ${failed} failed`)
}, 100)Реализуй функцию `createMock(implementation)` — создаёт мок-функцию как `vi.fn()`. Мок должен: вызывать `implementation` и сохранять каждый вызов в `mock.calls` (массив массивов аргументов), поддерживать `mockReturnValue(val)` (возвращает val при следующих вызовах), `mockImplementation(fn)` (заменяет реализацию), `mockReset()` (сбрасывает calls и возврат к оригинальной implementation). Каждый из методов возвращает сам мок для chaining.
currentImpl = () => val в mockReturnValue. В mockReset: currentImpl = implementation (замыкание на исходный параметр функции). mockFn.mock.calls — ссылка на тот же массив calls, поэтому calls.length = 0 очищает его и это отразится на mockFn.mock.calls.
Токены для AI-помощника закончились
Купи токены чтобы задавать вопросы AI прямо в уроке