Unit Testing bildet das Fundament einer robusten Teststrategie. Jest und Mocha sind die beiden dominierenden Frameworks für Unit Tests in der JavaScript-Entwicklung. Beide bieten umfangreiche Funktionalitäten, unterscheiden sich jedoch in ihrer Philosophie und ihrem Funktionsumfang.
Jest wurde von Facebook entwickelt und ist heute das meistgenutzte JavaScript-Testing-Framework. Es zeichnet sich durch seine “Zero-Configuration”-Philosophie aus und bringt alle notwendigen Tools für das Testen mit.
Die Installation von Jest erfolgt über npm:
npm install --save-dev jestDie einfachste package.json-Konfiguration:
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "^29.0.0"
}
}Jest erkennt automatisch Testdateien mit den Endungen
.test.js, .spec.js oder in
__tests__-Ordnern.
Jest verwendet eine intuitive, ausdrucksstarke Syntax für Tests:
// math.js
function add(a, b) {
return a + b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero');
}
return a / b;
}
module.exports = { add, multiply, divide };// math.test.js
const { add, multiply, divide } = require('./math');
describe('Math functions', () => {
test('should add two numbers correctly', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('should multiply two numbers correctly', () => {
expect(multiply(3, 4)).toBe(12);
expect(multiply(-2, 5)).toBe(-10);
});
test('should throw error when dividing by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero');
});
});Jest bietet eine umfangreiche Sammlung von Matchers für verschiedene Assertions:
describe('Jest Matchers Demo', () => {
test('equality matchers', () => {
expect(2 + 2).toBe(4); // Exakte Gleichheit
expect({ name: 'John' }).toEqual({ name: 'John' }); // Objektvergleich
});
test('truthiness matchers', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('Hello').toBeDefined();
});
test('number matchers', () => {
expect(2 + 2).toBeGreaterThan(3);
expect(Math.PI).toBeCloseTo(3.14, 2);
});
test('string matchers', () => {
expect('Hello World').toMatch(/World/);
expect('hello@example.com').toMatch(/^\w+@\w+\.\w+$/);
});
test('array matchers', () => {
expect(['apple', 'banana', 'orange']).toContain('banana');
expect([1, 2, 3]).toHaveLength(3);
});
});Jest bietet eingebaute Mocking-Funktionalitäten ohne externe Abhängigkeiten:
// userService.js
const apiClient = require('./apiClient');
class UserService {
async getUser(id) {
const response = await apiClient.get(`/users/${id}`);
return response.data;
}
async createUser(userData) {
const response = await apiClient.post('/users', userData);
return response.data;
}
}
module.exports = UserService;// userService.test.js
const UserService = require('./userService');
const apiClient = require('./apiClient');
// Mock des gesamten Moduls
jest.mock('./apiClient');
describe('UserService', () => {
let userService;
beforeEach(() => {
userService = new UserService();
jest.clearAllMocks();
});
test('should get user by id', async () => {
const mockUser = { id: 1, name: 'John Doe' };
apiClient.get.mockResolvedValue({ data: mockUser });
const result = await userService.getUser(1);
expect(apiClient.get).toHaveBeenCalledWith('/users/1');
expect(result).toEqual(mockUser);
});
test('should create new user', async () => {
const userData = { name: 'Jane Smith', email: 'jane@example.com' };
const createdUser = { id: 2, ...userData };
apiClient.post.mockResolvedValue({ data: createdUser });
const result = await userService.createUser(userData);
expect(apiClient.post).toHaveBeenCalledWith('/users', userData);
expect(result).toEqual(createdUser);
});
});Jest bietet Hooks für Setup und Teardown-Operationen:
describe('Database operations', () => {
let database;
beforeAll(async () => {
// Einmalige Vorbereitung für alle Tests
database = await connectToDatabase();
});
afterAll(async () => {
// Aufräumen nach allen Tests
await database.close();
});
beforeEach(() => {
// Vorbereitung vor jedem Test
database.beginTransaction();
});
afterEach(() => {
// Aufräumen nach jedem Test
database.rollback();
});
test('should save user to database', async () => {
const user = { name: 'Test User' };
const savedUser = await database.saveUser(user);
expect(savedUser).toHaveProperty('id');
expect(savedUser.name).toBe(user.name);
});
});Mocha ist ein etabliertes, flexibles Testing-Framework, das minimalistischer als Jest ist und Entwicklern mehr Kontrolle über ihre Testing-Pipeline gibt.
Mocha benötigt separate Bibliotheken für Assertions und Mocking:
npm install --save-dev mocha chai sinonGrundlegende package.json-Konfiguration:
{
"scripts": {
"test": "mocha"
},
"devDependencies": {
"mocha": "^10.0.0",
"chai": "^4.3.0",
"sinon": "^15.0.0"
}
}Chai bietet verschiedene Assertion-Stile:
// math.test.js
const { expect } = require('chai');
const { add, multiply, divide } = require('./math');
describe('Math functions', function() {
it('should add two numbers correctly', function() {
expect(add(2, 3)).to.equal(5);
expect(add(-1, 1)).to.equal(0);
});
it('should multiply two numbers correctly', function() {
expect(multiply(3, 4)).to.equal(12);
expect(multiply(-2, 5)).to.equal(-10);
});
it('should throw error when dividing by zero', function() {
expect(() => divide(10, 0)).to.throw('Division by zero');
});
});Chai bietet drei Assertion-Stile für unterschiedliche Präferenzen:
const { expect, should, assert } = require('chai');
describe('Chai assertion styles', function() {
const user = { name: 'John', age: 30, active: true };
it('expect style (BDD)', function() {
expect(user).to.be.an('object');
expect(user.name).to.equal('John');
expect(user.age).to.be.above(18);
expect(user).to.have.property('active', true);
});
it('should style (BDD)', function() {
should.exist(user);
user.should.be.an('object');
user.should.have.property('name', 'John');
});
it('assert style (TDD)', function() {
assert.isObject(user);
assert.equal(user.name, 'John');
assert.isTrue(user.active);
assert.property(user, 'age');
});
});Sinon bietet umfangreiche Mocking-Funktionalitäten für Mocha:
// userService.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const UserService = require('./userService');
const apiClient = require('./apiClient');
describe('UserService', function() {
let userService;
let apiStub;
beforeEach(function() {
userService = new UserService();
apiStub = sinon.stub(apiClient);
});
afterEach(function() {
sinon.restore();
});
it('should get user by id', async function() {
const mockUser = { id: 1, name: 'John Doe' };
apiStub.get.resolves({ data: mockUser });
const result = await userService.getUser(1);
expect(apiStub.get).to.have.been.calledWith('/users/1');
expect(result).to.deep.equal(mockUser);
});
it('should handle API errors gracefully', async function() {
apiStub.get.rejects(new Error('Network error'));
try {
await userService.getUser(1);
expect.fail('Should have thrown an error');
} catch (error) {
expect(error.message).to.equal('Network error');
}
});
});Sinon unterscheidet zwischen verschiedenen Test-Doubles:
describe('Sinon test doubles', function() {
it('should use spies to monitor function calls', function() {
const callback = sinon.spy();
const processor = new DataProcessor(callback);
processor.process(['data1', 'data2']);
expect(callback).to.have.been.calledTwice;
expect(callback.firstCall).to.have.been.calledWith('data1');
});
it('should use stubs to control function behavior', function() {
const fileSystem = { readFile: sinon.stub() };
fileSystem.readFile.withArgs('config.json').returns('{"setting": "value"}');
fileSystem.readFile.withArgs('missing.json').throws(new Error('File not found'));
const config = loadConfig('config.json', fileSystem);
expect(config.setting).to.equal('value');
});
it('should use mocks for strict verification', function() {
const logger = sinon.mock(console);
logger.expects('log').once().withArgs('Processing complete');
performTask();
logger.verify(); // Fails if expectations not met
});
});Zero Configuration: Jest funktioniert sofort ohne aufwändige Konfiguration. Es bringt alle notwendigen Tools mit: Test Runner, Assertions, Mocking und Code Coverage.
Snapshot Testing: Jest bietet eingebautes Snapshot Testing für React-Komponenten und andere serialisierbare Objekte.
Parallele Ausführung: Jest führt Tests standardmäßig parallel aus, was die Ausführungszeit reduziert.
Watch Mode: Intelligenter Watch Mode, der nur relevante Tests bei Änderungen ausführt.
// Jest Snapshot Test Beispiel
test('should render user component correctly', () => {
const user = { name: 'John', email: 'john@example.com' };
const rendered = renderUser(user);
expect(rendered).toMatchSnapshot();
});Flexibilität: Mocha ist modularer und erlaubt die freie Wahl von Assertion-Bibliotheken, Mocking-Tools und anderen Ergänzungen.
Browser-Support: Mocha kann direkt im Browser ausgeführt werden, was für bestimmte Testszenarien wichtig ist.
Vielseitige Reporter: Umfangreiche Auswahl an Report-Formaten und die Möglichkeit, eigene Reporter zu erstellen.
Asynchrone Tests: Mocha bietet verschiedene Ansätze für asynchrone Tests (Callbacks, Promises, async/await).
// Mocha asynchroner Test mit verschiedenen Patterns
describe('Async testing patterns', function() {
it('using callbacks', function(done) {
asyncFunction((error, result) => {
expect(error).to.be.null;
expect(result).to.equal('success');
done();
});
});
it('using promises', function() {
return asyncFunction()
.then(result => {
expect(result).to.equal('success');
});
});
it('using async/await', async function() {
const result = await asyncFunction();
expect(result).to.equal('success');
});
});// Jest: Parallele Tests und Test-Isolation
describe('Performance optimized tests', () => {
// Teure Setup-Operationen einmal durchführen
let expensiveResource;
beforeAll(async () => {
expensiveResource = await createExpensiveResource();
});
// Leichtgewichtige Tests
test('quick operation test', () => {
const result = expensiveResource.quickOperation();
expect(result).toBeDefined();
});
// Nur bei Bedarf aufwändige Operationen
test('expensive operation test', async () => {
const result = await expensiveResource.expensiveOperation();
expect(result).toHaveProperty('status', 'completed');
}, 10000); // Timeout erhöhen für langsame Tests
});// Debug-Ausgaben in Tests
describe('Debugging example', () => {
test('debug failing test', () => {
const data = processData(inputData);
// Temporäre Debug-Ausgabe
console.log('Processed data:', JSON.stringify(data, null, 2));
expect(data.status).toBe('success');
});
});