Yerelde İşçilerin Test Edilmesi: Miniflare, Vitest ve Wrangler Dev
Miniflare ve Vitest ile İşçileri yerel olarak test etmek için eksiksiz bir iş akışı: işleyici birim testleri, simüle edilmiş bağlamalar ve hata ayıklama ile entegrasyon testleri Cloudflare'de dağıtım olmadan wrangler dev ile.
Uçta Test Sorunu
Cloudflare Worker'ı test etmek, geleneksel Node.js testine kıyasla benzersiz zorluklar sunar.
Workers çalışma zamanı Node.js değildir: Web Platformu API'lerinin bir alt kümesini açığa çıkarır,
dosya sistemi erişimi, kullanılmaz require() ve farklı bir yürütme modeline sahiptir.
Bu, Jest gibi standart koşucunun kendi ortamıyla test ettiği anlamına gelir jsdom
o düğüm gerçek yürütme ortamını doğru şekilde simüle etmezler.
Cloudflare ekosistem çözümü Mini alev, bir simülatör üzerine inşa edilen Workers çalışma zamanının yereli çalışan. Mini flare de aynı şeyi sergiliyor Üretimde bulduğunuz API'ler (KV, R2, D1, Dayanıklı Nesneler, Önbellek API'si, bağlamalar) Cloudflare bağlantısı gerektirmeden yerel ortam.
Ne Öğreneceksiniz
- Vitest + Miniflare ile proje kurulumu (wrangler.toml konfigürasyonu)
- Simüle edilmiş isteklerle getirme işleyicisinin birim testi
- KV, R2 ve D1'i bellek içi depoyla test etme
- Simüle edilmiş oturumlarla Dayanıklı Nesneler testi
- Testleri harici hizmetlerden yalıtmak için fetch() ile alay etme
- Denetçiyle etkileşimli geliştirme ve hata ayıklama için Wrangler geliştiricisi
- GitHub Eylemlerini içeren CI/CD: otomatik dağıtım öncesi testi
Kurulum: Vitest + @cloudflare/vitest-pool-workers
Cloudflare 2024'ten beri kullanıma sunuldu @cloudflare/vitest-pool-workers,
Ortam olarak Workd'ü kullanan bir Vitest havuzu. Bu yaklaşımın yerine geçer
önceden bağımsız bir kitaplık olarak Miniflare'i temel alıyordu - artık her şey entegre
Vitest iş akışında.
# Installa le dipendenze di sviluppo
npm install --save-dev vitest @cloudflare/vitest-pool-workers
# Struttura progetto consigliata
my-worker/
src/
index.ts # Entry point Worker
handlers/
users.ts
products.ts
utils/
auth.ts
validation.ts
test/
index.test.ts
handlers/
users.test.ts
integration/
api.test.ts
wrangler.toml
vitest.config.ts
tsconfig.json
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
// Usa il pool Cloudflare invece dell'ambiente Node.js
pool: '@cloudflare/vitest-pool-workers',
poolOptions: {
workers: {
// Punta al wrangler.toml del tuo Worker
wrangler: { configPath: './wrangler.toml' },
},
},
},
});
# wrangler.toml - configurazione che viene letta dal pool di test
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2025-01-01"
compatibility_flags = ["nodejs_compat"]
# Binding KV usato nei test
[[kv_namespaces]]
binding = "USERS_KV"
id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# Importante: in test, il pool usa automaticamente KV in-memory
# D1 Database
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy"
# R2 Bucket
[[r2_buckets]]
binding = "ASSETS_BUCKET"
bucket_name = "my-bucket"
# Variabili d'ambiente
[vars]
ENVIRONMENT = "test"
API_VERSION = "v1"
Birim Testi: Temel Getirme İşleyicisi
En yaygın test, getirme işleyicisinin doğru yanıt verdiğini doğrulamaktır. farklı türdeki isteklere. Cloudflare havuzuyla test, çalışma ortamında yürütülür gerçek.
// test/index.test.ts
import { describe, it, expect } from 'vitest';
import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test';
import worker from '../src/index';
// "env" fornisce i binding configurati in wrangler.toml (in-memory per i test)
// "SELF" e il Worker corrente, utile per integration test
describe('Worker fetch handler', () => {
it('returns 200 for GET /', async () => {
const request = new Request('http://example.com/');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
// Attendi il completamento dei waitUntil()
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
});
it('returns 404 for unknown routes', async () => {
const request = new Request('http://example.com/unknown-route');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(404);
});
it('returns 405 for POST to GET-only endpoint', async () => {
const request = new Request('http://example.com/api/users', {
method: 'POST',
body: JSON.stringify({ name: 'Test' }),
headers: { 'Content-Type': 'application/json' },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(405);
});
it('returns JSON with correct Content-Type', async () => {
const request = new Request('http://example.com/api/health');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.headers.get('Content-Type')).toContain('application/json');
const body = await response.json();
expect(body).toHaveProperty('status', 'ok');
});
});
KV Bağlamaları Bellek İçi ile Test Etme
Cloudflare havuzu, testler arasında sıfırlanan bir bellek içi depo ile KV'yi simüle eder. Test etmeden önce mağazayı önceden doldurabilir ve Çalışanın okuyup yazdığını doğrulayabilirsiniz. doğru.
// src/handlers/users.ts - il codice da testare
export async function handleGetUser(
userId: string,
env: Env
): Promise<Response> {
const user = await env.USERS_KV.get(`user:${userId}`);
if (!user) {
return Response.json({ error: 'User not found' }, { status: 404 });
}
return Response.json(JSON.parse(user));
}
export async function handleCreateUser(
request: Request,
env: Env
): Promise<Response> {
const body = await request.json() as { name: string; email: string };
if (!body.name || !body.email) {
return Response.json({ error: 'name and email required' }, { status: 400 });
}
const userId = crypto.randomUUID();
const user = { id: userId, ...body, createdAt: Date.now() };
await env.USERS_KV.put(`user:${userId}`, JSON.stringify(user));
return Response.json(user, { status: 201 });
}
interface Env {
USERS_KV: KVNamespace;
}
// test/handlers/users.test.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { env } from 'cloudflare:test';
import { handleGetUser, handleCreateUser } from '../../src/handlers/users';
describe('User handlers', () => {
// Prepopola KV prima di ogni test
beforeEach(async () => {
await env.USERS_KV.put('user:test-id-123', JSON.stringify({
id: 'test-id-123',
name: 'Mario Rossi',
email: 'mario@example.com',
createdAt: 1700000000000,
}));
});
describe('GET user', () => {
it('returns user when found', async () => {
const response = await handleGetUser('test-id-123', env);
expect(response.status).toBe(200);
const body = await response.json() as { id: string; name: string };
expect(body.id).toBe('test-id-123');
expect(body.name).toBe('Mario Rossi');
});
it('returns 404 when user not found', async () => {
const response = await handleGetUser('non-existent', env);
expect(response.status).toBe(404);
const body = await response.json() as { error: string };
expect(body.error).toBe('User not found');
});
});
describe('POST user', () => {
it('creates user and returns 201', async () => {
const request = new Request('http://example.com/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Luca Bianchi', email: 'luca@example.com' }),
});
const response = await handleCreateUser(request, env);
expect(response.status).toBe(201);
const body = await response.json() as { id: string; name: string };
expect(body.name).toBe('Luca Bianchi');
expect(body.id).toBeTruthy();
// Verifica che sia stato effettivamente salvato in KV
const stored = await env.USERS_KV.get(`user:${body.id}`);
expect(stored).not.toBeNull();
});
it('returns 400 when name is missing', async () => {
const request = new Request('http://example.com/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'only-email@example.com' }),
});
const response = await handleCreateUser(request, env);
expect(response.status).toBe(400);
});
});
});
D1 ile test etme (Uçta SQLite)
D1, Cloudflare'in SQLite veritabanıdır. Test havuzu bir veritabanı oluşturur her çalıştırma için bellekte. Test etmeden önce taşıma ve tohumlamayı çalıştırabilirsiniz.
// test/integration/d1.test.ts
import { describe, it, expect, beforeAll } from 'vitest';
import { env } from 'cloudflare:test';
describe('D1 database operations', () => {
// Crea lo schema prima dei test
beforeAll(async () => {
await env.DB.exec(`
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL,
category TEXT,
created_at INTEGER DEFAULT (unixepoch())
)
`);
// Seed con dati di test
await env.DB.prepare(
'INSERT INTO products (id, name, price, category) VALUES (?, ?, ?, ?)'
).bind('prod-1', 'Widget Pro', 29.99, 'widgets').run();
await env.DB.prepare(
'INSERT INTO products (id, name, price, category) VALUES (?, ?, ?, ?)'
).bind('prod-2', 'Gadget Plus', 49.99, 'gadgets').run();
});
it('retrieves all products', async () => {
const result = await env.DB.prepare('SELECT * FROM products').all();
expect(result.results).toHaveLength(2);
});
it('filters products by category', async () => {
const result = await env.DB
.prepare('SELECT * FROM products WHERE category = ?')
.bind('widgets')
.all();
expect(result.results).toHaveLength(1);
expect(result.results[0]).toMatchObject({ name: 'Widget Pro' });
});
it('inserts a new product', async () => {
await env.DB.prepare(
'INSERT INTO products (id, name, price, category) VALUES (?, ?, ?, ?)'
).bind('prod-3', 'New Item', 9.99, 'misc').run();
const result = await env.DB.prepare(
'SELECT * FROM products WHERE id = ?'
).bind('prod-3').first();
expect(result).toMatchObject({ name: 'New Item', price: 9.99 });
});
});
Testleri Yalıtmak için fetch() ile alay etme
Workers çalışma zamanı bir işlevi kullanıma sunuyor fetch() HTTP çağrıları için global.
Test sırasında, gerçek aramalar yapmamak için bunu bir sahte ile değiştirmek istiyorsunuz.
dış hizmetler. Miniflare, sahte yolu destekler fetchMock.
// test/integration/api.test.ts - Mocking di fetch esterno
import { describe, it, expect, vi, afterEach } from 'vitest';
import { env, createExecutionContext, waitOnExecutionContext, fetchMock } from 'cloudflare:test';
import worker from '../../src/index';
// Abilita il mock di fetch prima di tutti i test del file
fetchMock.activate();
fetchMock.disableNetConnect(); // Blocca connessioni reali
afterEach(() => {
fetchMock.assertNoPendingInterceptors();
});
describe('External API integration', () => {
it('proxies weather API with caching', async () => {
// Configura il mock: quando il Worker chiama questo URL, ritorna questa risposta
fetchMock
.get('https://api.weather.example.com')
.intercept({ path: '/v1/current?city=Milan' })
.reply(200, {
city: 'Milan',
temperature: 18,
condition: 'Cloudy',
}, {
headers: { 'Content-Type': 'application/json' },
});
const request = new Request('http://my-worker.example.com/weather?city=Milan');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
const body = await response.json() as { city: string; temperature: number };
expect(body.city).toBe('Milan');
expect(body.temperature).toBe(18);
});
it('handles upstream API errors gracefully', async () => {
fetchMock
.get('https://api.weather.example.com')
.intercept({ path: '/v1/current?city=Unknown' })
.reply(503, { error: 'Service Unavailable' });
const request = new Request('http://my-worker.example.com/weather?city=Unknown');
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
// Il Worker deve gestire l'errore e ritornare una risposta coerente
expect(response.status).toBe(503);
const body = await response.json() as { error: string };
expect(body.error).toContain('upstream');
});
});
Kimlik Doğrulama ve Başlık Testi
// test/handlers/auth.test.ts
import { describe, it, expect } from 'vitest';
import { env, createExecutionContext, waitOnExecutionContext } from 'cloudflare:test';
import worker from '../../src/index';
describe('Authentication middleware', () => {
it('rejects requests without Authorization header', async () => {
const request = new Request('http://example.com/api/protected', {
method: 'GET',
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(401);
const body = await response.json() as { error: string };
expect(body.error).toContain('unauthorized');
});
it('rejects requests with invalid token', async () => {
const request = new Request('http://example.com/api/protected', {
headers: { 'Authorization': 'Bearer invalid-token-xyz' },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(403);
});
it('allows requests with valid API key from KV', async () => {
// Prepopola KV con un API key valido
await env.USERS_KV.put('apikey:valid-api-key-abc', JSON.stringify({
userId: 'user-1',
tier: 'pro',
expiresAt: Date.now() + 3600000,
}));
const request = new Request('http://example.com/api/protected', {
headers: { 'Authorization': 'Bearer valid-api-key-abc' },
});
const ctx = createExecutionContext();
const response = await worker.fetch(request, env, ctx);
await waitOnExecutionContext(ctx);
expect(response.status).toBe(200);
});
});
SELF ile Entegrasyon Testi
SELF yapmanıza izin veren mevcut Çalışana bir referanstır
Harici bir istemcinin HTTP istekleri, uçtan uca testler için faydalıdır
tüm yönlendiricinin.
// test/integration/e2e.test.ts
import { describe, it, expect } from 'vitest';
import { SELF } from 'cloudflare:test';
// SELF.fetch() chiama il Worker come se fosse una richiesta HTTP reale
// Passa per l'intero stack: middleware, routing, handler
describe('End-to-end API flow', () => {
it('full user CRUD flow', async () => {
// 1. Crea un utente
const createResponse = await SELF.fetch('http://example.com/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Test User', email: 'test@example.com' }),
});
expect(createResponse.status).toBe(201);
const created = await createResponse.json() as { id: string; name: string };
const userId = created.id;
// 2. Leggi l'utente
const getResponse = await SELF.fetch(`http://example.com/api/users/${userId}`);
expect(getResponse.status).toBe(200);
const fetched = await getResponse.json() as { name: string };
expect(fetched.name).toBe('Test User');
// 3. Aggiorna l'utente
const updateResponse = await SELF.fetch(`http://example.com/api/users/${userId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Updated User' }),
});
expect(updateResponse.status).toBe(200);
// 4. Elimina l'utente
const deleteResponse = await SELF.fetch(`http://example.com/api/users/${userId}`, {
method: 'DELETE',
});
expect(deleteResponse.status).toBe(204);
// 5. Verifica che non esista più
const notFoundResponse = await SELF.fetch(`http://example.com/api/users/${userId}`);
expect(notFoundResponse.status).toBe(404);
});
});
Wrangler Dev: Etkileşimli Geliştirme
wrangler dev Workers çalışma zamanını simüle eden yerel bir sunucuyu başlatır
tüm bağlamalar dahil eksiksiz. 2024'ten itibaren doğrudan Workd'ü kullanın, ardından
çevre üretimle aynıdır.
# Avvia il server di sviluppo locale
npx wrangler dev
# Con binding remoti (usa i dati reali di KV/D1 dall'account Cloudflare)
npx wrangler dev --remote
# Con porta specifica e inspector per Chrome DevTools
npx wrangler dev --port 8787 --inspector-port 9229
# Con variabili d'ambiente da file .dev.vars
# .dev.vars (non committare in git!)
# API_SECRET=my-local-secret
# DATABASE_URL=postgresql://localhost:5432/mydb
npx wrangler dev
# wrangler.toml - configurazione per sviluppo locale
[dev]
port = 8787
local_protocol = "http"
# upstream = "https://my-staging-api.example.com" # Proxy verso staging
# KV locale: usa --persist per salvare i dati tra restart
# npx wrangler dev --persist
# I dati vengono salvati in .wrangler/state/
# D1 locale: crea un file SQLite locale
[[d1_databases]]
binding = "DB"
database_name = "my-db"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# In locale, wrangler crea automaticamente .wrangler/state/v3/d1/
# Secrets in locale: usa il file .dev.vars
# [vars] nel wrangler.toml e per valori non segreti
Wrangler dev --local ve --remote arasındaki farklar
- --local (varsayılan): Tüm bağlamalar bellek içi/yereldir.
Hızlı, sıfır maliyet, çevrimdışı çalışın. Yeniden başlatmalar arasında veriler kalıcı olmuyor
(tabii ki
--persist). - --uzak: Kod yerel olarak çalışır ancak bağlamalar (KV, D1, R2) Cloudflare hesabınızdaki gerçek verilere işaret ederler. İle test etmek için kullanışlıdır gerçek veridir ancak internet bağlantısı gerektirir ve kota tüketir.
CI/CD: Otomatik Test için GitHub Eylemleri
# .github/workflows/test.yml
name: Test Cloudflare Workers
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run type check
run: npx tsc --noEmit
- name: Run unit and integration tests
run: npx vitest run --reporter=verbose
- name: Upload coverage
uses: codecov/codecov-action@v4
if: always()
with:
token: ${{ secrets.CODECOV_TOKEN }}
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Deploy to Cloudflare Workers
run: npx wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
Package.json: Önerilen Test Komut Dosyaları
{
"scripts": {
"dev": "wrangler dev",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"test:ui": "vitest --ui",
"typecheck": "tsc --noEmit",
"lint": "eslint src --ext .ts",
"build": "wrangler deploy --dry-run",
"deploy": "wrangler deploy",
"deploy:staging": "wrangler deploy --env staging"
},
"devDependencies": {
"@cloudflare/vitest-pool-workers": "^0.5.0",
"@cloudflare/workers-types": "^4.0.0",
"typescript": "^5.4.0",
"vitest": "^1.6.0",
"wrangler": "^3.50.0"
}
}
İşçi Testlerinde Yaygın Anti-Paternler
Kaçınılması Gereken Hatalar
-
Beklemeyin waitOnExecutionContext(): I
ctx.waitUntil()getirme işleyicisi geri döndükten sonra devam ederler. Olmadanawait waitOnExecutionContext(ctx)testlerde asenkron işlemler (KV'ye kaydetme gibi) tamamlanmayabilir iddialardan önce. - Vitest yerine Jest'i kullanmak: Jest yerel olarak havuzlamayı desteklemiyor Bulut parlaması. Jest'ten geçiş yapıyorsanız Vitest'in uyumluluk modunu kullanın.
-
Değişken küresel durumla test edin: Aynı izolatların olduğunu unutmayın
birden fazla isteği karşılayabilir. Testler bağımsız olmalıdır: kullanın
beforeEachKV/D1 durumunu sıfırlamak için. -
Testlerde gerçek HTTP çağrıları: Amerika
fetchMockefetchMock.disableNetConnect()testlerin yapıldığından emin olmak için Belirleyicidir ve dış hizmetlere bağlı değildir.
Sonuçlar ve Sonraki Adımlar
Vitest'le + @cloudflare/vitest-pool-workers, senin bir
Gerçek çalışan ortamında çalışan Çalışanlar için profesyonel test iş akışı,
bağlama davranışını doğrulamak için konuşlandırma ihtiyacını ortadan kaldırır
ve CI/CD ile sorunsuz bir şekilde bütünleşir. Kurulum maliyeti düşüktür, geri bildirim
hızlıdır ve testler üretim ortamına sadıktır.
Serideki Sonraki Yazılar
- Madde 10: Uçta Tam Yığın Mimariler — Örnek Olay İncelemesi Sıfırdan Üretime: İşçiler, D1, R2, kimlik doğrulama ile eksiksiz bir uygulama JWT ve otomatik CI/CD.







