🔧 Programmierung 🕛 kürzlich 8 Min Lesezeit
0

Testing JavaScript: A Practical Guide to TDD with Jest (2026)

↗ Quelle (dev.to)
🗣️ Stimme:
📑 Inhaltsübersicht




Testing JavaScript: A Practical Guide to TDD with Jest (2026)



Testing isn't optional anymore. Here's how to actually do it — with real examples you can copy.






Why Test?






CODE
Without tests:
→ You're scared to refactor (what if I break something?)
→ Bugs get reintroduced ("regressions")
→ Code reviews take forever (reviewers have to mentally trace logic)
→ Onboarding is slow (no tests document expected behavior)
→ Deployments are stressful (did I break production?)

With tests:
→ Refactor confidently (tests tell you if something broke)
→ Catches regressions immediately
→ Code reviews focus on design, not correctness
→ Tests ARE documentation (executable specs)
→ Deployment is boring (in a good way) — just another day









Setup






CODE
# Install Jest
npm install --save-dev jest
# Or with TypeScript support:
npm install --save-dev jest ts-jest @types/jest

# Add to package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}

# Run tests:
npm test # Run once
npm run test:watch # Watch mode (re-runs on file changes)
npm run test:coverage # With coverage report









CODE
// jest.config.js (or in package.json under "jest")
module.exports = {
// Where to find tests
testMatch: ['**/__tests__/**/*.js', '**/*.test.js', '**/*.spec.js'],

// Transform files (TypeScript, JSX, etc.)
transform: {
'^.+\\.jsx?$': 'babel-jest', // or 'ts-jest' for TypeScript
},

// Coverage settings
collectCoverageFrom: [
'src/**/*.{js,ts}',
'!src/**/*.test.{js,ts}',
'!src/index.ts', // Entry point usually doesn't need coverage
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},

// Test environment (jsdom for DOM tests, node for pure JS)
testEnvironment: 'node',

// Setup file (runs before each test)
setupFilesAfterEnv: ['./tests/setup.js'],
};









The Three Types of Tests You Need






Unit Tests — Test One Thing in Isolation






CODE
// utils/price.js
export function calculateTotal(items, taxRate = 0.1) {
if (!items || items.length === 0) return 0;
const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0);
return subtotal * (1 + taxRate);
}

export function formatPrice(cents) {
const dollars = cents / 100;
return `$${dollars.toFixed(2)}`;
}

export function applyDiscount(total, discountCode) {
const discounts = { SAVE10: 0.1, SAVE20: 0.2, WELCOME: 0.25 };
if (!discounts[discountCode]) return total; // Invalid code = no discount
return total * (1 - discounts[discountCode]);
}









CODE
// __tests__/utils/price.test.js
const { calculateTotal, formatPrice, applyDiscount } = require('../../utils/price');

describe('calculateTotal', () => {
it('should return 0 for empty array', () => {
expect(calculateTotal([])).toBe(0);
});

it('should return 0 for null/undefined', () => {
expect(calculateTotal(null)).toBe(0);
expect(calculateTotal(undefined)).toBe(0);
});

it('should calculate subtotal with default tax', () => {
const items = [{ price: 100, quantity: 2 }];
expect(calculateTotal(items)).toBe(220); // 200 + 10% tax
});

it('should handle multiple items correctly', () => {
const items = [
{ price: 1000, quantity: 1 },
{ price: 500, quantity: 3 },
{ price: 50, quantity: 2 },
];
// Subtotal: 1000 + 1500 + 100 = 2600
// With 10% tax: 2860
expect(calculateTotal(items)).toBe(2860);
});

it('should use custom tax rate when provided', () => {
const items = [{ price: 100, quantity: 1 }];
expect(calculateTotal(items, 0.2)).toBe(120); // 100 + 20% tax
});
});

describe('formatPrice', () => {
it('should format cents to dollars', () => {
expect(formatPrice(1999)).toBe('$19.99');
expect(formatPrice(100)).toBe('$1.00');
expect(formatPrice(0)).toBe('$0.00');
});

it('should round to 2 decimal places', () => {
expect(formatPrice(333)).toBe('$3.33'); // 3.33 not 3.333...
});
});

describe('applyDiscount', () => {
it('should apply SAVE10 discount', () => {
expect(applyDiscount(100, 'SAVE10')).toBe(90);
});

it('should apply SAVE20 discount', () => {
expect(applyDiscount(100, 'SAVE20')).toBe(80);
});

it('should apply WELCOME discount (best deal)', () => {
expect(applyDiscount(100, 'WELCOME')).toBe(75);
});

it('should return original total for invalid code', () => {
expect(applyDiscount(100, 'INVALID')).toBe(100);
expect(applyDiscount(100, '')).toBe(100);
expect(applyDiscount(100, null)).toBe(100);
});
});









Integration Tests — Test Components Working Together






CODE
// services/orderService.js
const db = require('./db');
const { calculateTotal, applyDiscount } = require('../utils/price');

async function createOrder(userId, items, discountCode) {
const user = await db.users.findById(userId);
if (!user) throw new Error('User not found');

let total = calculateTotal(items);
if (discountCode) {
total = applyDiscount(total, discountCode);
}

const order = await db.orders.create({
userId,
items,
total,
status: 'pending',
});

return order;
}









CODE
// __tests__/services/orderService.test.js
const { createOrder } = require('../../services/orderService');

// Mock the database layer
jest.mock('../../services/db');

describe('createOrder', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('should create order successfully', async () => {
const mockUser = { id: 'user_1', name: 'Test User' };
const mockOrder = { id: 'order_1', total: 110, status: 'pending' };

require('../../services/db').users.findById.mockResolvedValue(mockUser);
require('../../services/db').orders.create.mockResolvedValue(mockOrder);

const result = await createOrder(
'user_1',
[{ price: 100, quantity: 1 }]
);

expect(result).toEqual(mockOrder);
expect(require('../../services/db').orders.create).toHaveBeenCalledWith(
expect.objectContaining({ total: 110, status: 'pending' })
);
});

it('should throw error for non-existent user', async () => {
require('../../services/db').users.findById.mockResolvedValue(null);

await expect(createOrder('bad_id', []))
.rejects.toThrow('User not found');
});

it('should apply discount code when provided', async () => {
require('../../services/db').users.findById.mockResolvedValue({ id: 'u1' });
require('../../services/db').orders.create.mockImplementation(o => Promise.resolve({...o, id: 'o1'}));

const result = await createOrder(
'u1',
[{ price: 100, quantity: 1 }],
'SAVE10'
);

expect(result.total).toBe(90); // $100 - 10%
});
});









E2E / API Tests — Test Real HTTP Endpoints






CODE
// __tests__/api/orders.e2e.test.js
const request = require('supertest');
const app = require('../../app');

describe('POST /api/orders', () => {
it('should create an order and return 201', async () => {
const res = await request(app)
.post('/api/orders')
.send({
userId: 'user_1',
items: [{ productId: 'prod_1', quantity: 2 }],
discountCode: 'SAVE10',
})
.expect('Content-Type', /json/)
.expect(201);

expect(res.body).toMatchObject({
status: 'pending',
items: expect.arrayContaining([
expect.objectContaining({ quantity: 2 }),
]),
});
expect(res.body.total).toBeGreaterThan(0);
expect(res.body.id).toBeDefined();
});

it('should return 400 for invalid input', async () => {
const res = await request(app)
.post('/api/orders')
.send({}) // Missing required fields
.expect(400);

expect(res.body.error).toBeDefined();
});

it('should return 404 for non-existent user', async () => {
const res = await request(app)
.post('/api/orders')
.send({
userId: 'nonexistent_user',
items: [{ productId: 'p1', quantity: 1 }],
})
.expect(404);
});
});









TDD Workflow (Test-Driven Development)






CODE
RED → GREEN → REFACTOR (repeat)

1. RED: Write a failing test for the behavior you want
2. GREEN: Write the MINIMUM code to make the test pass
3. REFACTOR: Improve the code while keeping tests green
4. Repeat for next feature









CODE
// Example: Building a password validator via TDD

// Step 1: RED — Write failing test
describe('PasswordValidator', () => {
it('should reject passwords shorter than 8 characters', () => {
expect(validatePassword('abc123')).toBe(false);
});

it('should accept valid 8+ character passwords', () => {
expect(validatePassword('Abc123!@')).toBe(true);
});
});
// Run: FAILS (validatePassword doesn't exist yet)

// Step 2: GREEN — Minimum implementation
function validatePassword(password) {
return password.length >= 8;
}
// Run: PASSES ✅

// Step 3: Add more tests (back to RED)
it('should require at least one uppercase letter', () => {
expect(validatePassword('abcdefg1!')).toBe(false);
});

it('should require at least one number', () => {
expect(validatePassword('Abcdefg!')).toBe(false);
});
// Run: FAILS ❌

// Step 4: GREEN again
function validatePassword(password) {
if (password.length < 8) return false;
if (!/[A-Z]/.test(password)) return false;
if (!/[0-9]/.test(password)) return false;
return true;
}
// Run: PASSES ✅

// Step 5: REFACTOR (make cleaner without breaking tests)
function validatePassword(password) {
const rules = [
{ test: p => p.length >= 8, error: 'min 8 chars' },
{ test: p => /[A-Z]/.test(p), error: 'uppercase required' },
{ test: p => /[0-9]/.test(p), error: 'number required' },
];
return rules.every(rule => rule.test(password));
}
// Run: Still PASSES ✅ (refactoring didn't break anything!)









Common Patterns & Matchers






CODE
// Object shape matching (great for API responses)
expect(response).toMatchObject({
status: 'success',
data: expect.any(Array),
pagination: expect.objectContaining({ page: 1 }),
});

// Async testing
it('fetches data asynchronously', async () => {
const data = await fetchData('/api/users');
expect(data).toHaveLength(5);
});

// Error handling
it('throws with specific message', () => {
expect(() => riskyOperation()).toThrow('specific error text');
});

await expect(asyncFunction()).rejects.toThrow(ErrorType);

// Callback-style
it('calls callback with data', (done) => {
asyncWithCallback((err, data) => {
expect(err).toBeNull();
expect(data).toBeDefined();
done(); // Must call done()!
});
});

// Timers
beforeEach(() => jest.useFakeTimers());
afterEach(() => jest.useRealTimers());

it('debounces function calls', () => {
const fn = jest.fn();
const debouncedFn = debounce(fn, 500);

debouncedFn(); // Call 1
debouncedFn(); // Call 2
debouncedFn(); // Call 3

expect(fn).not.toHaveBeenCalled(); // Not called yet!

jest.advanceTimersByTime(500);
expect(fn).toHaveBeenCalledTimes(1); // Called once after delay!
});

// Spies
it('should call logger on error', () => {
const loggerSpy = jest.spyOn(console, 'error');
processWithError();
expect(loggerSpy).toHaveBeenCalledWith(expect.stringContaining('failed'));
loggerSpy.mockRestore(); // Always restore!
});









Testing Best Practices






CODE
 DO:
- Test behavior, not implementation (refactor-friendly)
- Use descriptive test names (reads like a sentence)
- One assertion per concept (use describe blocks)
- Test edge cases: null, empty, boundary values
- Keep tests fast (< 100ms per test ideally)
- Use meaningful test data (not always "foo", "bar")
- Setup/tear down in beforeEach/afterEach

DON'T:
- Test private methods (test public behavior instead)
- Make tests depend on each other (isolation!)
- Ignore flaky tests (fix or delete them)
- Put logic in tests (if/else in tests = smell)
- Mock everything (integration tests need real connections)
- Skip writing tests because "it
's simple"
- Use random data in tests (non-deterministic = flaky)

📏 Structure each test file:
describe('Module/Component', () => {
describe('functionName()', () => {
it('should do X when Y', () => {});
it('should throw when Z', () => {});
});
});









What's your testing philosophy? TDD purist or test-after?



Follow @armorbreak for more practical developer guides.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 40%
🟡 In Evaluierung 34%
🟢 Keine Auswirkung 15%
Spannende Innovation 11%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
2 Quellen
Creator Panel – One Creator, Full Production: Der neue Creator Workflow
1 Quelle
ChatGPT showing blank screen [Fix]
1 Quelle
Sofort deinstallieren: Diese 19 Browser-Erweiterungen sind mit Malware verseucht
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Testing JavaScript: A Practical Guide to TDD with Jest (2026)

Thematisch verwandte Begriffe: Testing, JavaScript, Practical, Guide · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...