Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
- Basic knowledge of testing concepts 🧪
What you'll learn
- Understand Istanbul code coverage fundamentals 🎯
- Generate and interpret coverage reports 📊
- Set up coverage thresholds for quality control 🛡️
- Debug uncovered code paths 🔍
🎯 Introduction
Welcome to the world of code coverage with Istanbul! 🎉 If you’ve ever wondered whether your tests are actually testing your code (and not just passing by luck), you’re in for a treat!
Code coverage is like having a detective 🕵️♂️ that follows your tests around and reports back: “Hey, your tests visited 85% of your code, but this sneaky function over here? Totally ignored!” Istanbul is one of the most powerful and popular code coverage tools for JavaScript and TypeScript projects.
By the end of this tutorial, you’ll know how to generate beautiful coverage reports, interpret the numbers like a pro, and set up quality gates that keep your codebase healthy! Let’s dive in! 🏊♂️
📚 Understanding Code Coverage
🤔 What is Code Coverage?
Code coverage is like having a fitness tracker 📱 for your code! Just as a fitness tracker tells you how many steps you’ve taken, code coverage tells you which lines, branches, and functions your tests have “visited” during execution.
Think of it this way: Imagine your code is a house 🏠 with many rooms. Code coverage shows you which rooms your tests actually walked through. Did they check the kitchen ✨, peek into the bedroom 🛏️, but completely ignore the basement? That’s what coverage reports reveal!
In TypeScript terms, Istanbul tracks four main types of coverage:
- ✨ Line Coverage: Which lines of code were executed
- 🌟 Function Coverage: Which functions were called
- 🎯 Branch Coverage: Which code paths (if/else branches) were taken
- 📊 Statement Coverage: Which statements were executed
💡 Why Use Istanbul for Code Coverage?
Here’s why developers love Istanbul:
- Quality Assurance 🛡️: Know exactly what your tests cover
- Visual Reports 📊: Beautiful HTML reports that are easy to understand
- Multiple Formats 📋: HTML, JSON, text, and more output formats
- Threshold Enforcement ⚡: Fail builds if coverage drops below standards
- TypeScript Support 💙: Works seamlessly with TypeScript projects
Real-world example: Imagine building an e-commerce checkout system 🛒. With Istanbul, you can verify that your tests cover all payment methods, error scenarios, and edge cases!
🔧 Basic Setup and Usage
📦 Installation
Let’s start by setting up Istanbul in your TypeScript project:
# 📦 Install Istanbul CLI and NYC (the command-line interface)
npm install --save-dev nyc
# 🎯 For TypeScript support
npm install --save-dev @istanbuljs/nyc-config-typescript
npm install --save-dev ts-node
# 🧪 If you're using Jest (common setup)
npm install --save-dev jest @types/jest ts-jest
🔧 Basic Configuration
Create a .nycrc.json
file in your project root:
{
"extends": "@istanbuljs/nyc-config-typescript",
"check-coverage": true,
"all": true,
"include": [
"src/**/*.ts"
],
"exclude": [
"**/*.d.ts",
"**/*.test.ts",
"**/*.spec.ts",
"**/node_modules/**"
],
"reporter": [
"html",
"text",
"json"
],
"report-dir": "coverage"
}
💡 Explanation: This config tells Istanbul to include all TypeScript files in src/
, exclude test files, and generate HTML, text, and JSON reports!
🎯 Package.json Scripts
Add these helpful scripts to your package.json
:
{
"scripts": {
"test": "jest",
"test:coverage": "nyc npm test",
"coverage:html": "nyc --reporter=html npm test",
"coverage:check": "nyc check-coverage --lines 80 --functions 80 --branches 80"
}
}
💡 Practical Examples
🛒 Example 1: E-commerce Cart Coverage
Let’s create a shopping cart and see how Istanbul tracks our test coverage:
// 🛍️ src/cart.ts - Our shopping cart implementation
export interface Product {
id: string;
name: string;
price: number;
category: "electronics" | "books" | "clothing";
emoji: string;
}
export interface CartItem extends Product {
quantity: number;
}
export class ShoppingCart {
private items: CartItem[] = [];
private discountCode?: string;
// ➕ Add item to cart
addItem(product: Product, quantity: number = 1): void {
const existingItem = this.items.find(item => item.id === product.id);
if (existingItem) {
// 🔄 Update existing item quantity
existingItem.quantity += quantity;
} else {
// ✨ Add new item
this.items.push({ ...product, quantity });
}
}
// ➖ Remove item from cart
removeItem(productId: string): boolean {
const index = this.items.findIndex(item => item.id === productId);
if (index === -1) {
return false; // 🚫 Item not found
}
this.items.splice(index, 1);
return true; // ✅ Item removed
}
// 💰 Calculate total with optional discount
getTotal(): number {
const subtotal = this.items.reduce(
(sum, item) => sum + (item.price * item.quantity),
0
);
// 🎟️ Apply discount if available
if (this.discountCode === "SAVE20") {
return subtotal * 0.8; // 20% off
} else if (this.discountCode === "SAVE10") {
return subtotal * 0.9; // 10% off
}
return subtotal;
}
// 🎟️ Apply discount code
applyDiscount(code: string): boolean {
if (code === "SAVE20" || code === "SAVE10") {
this.discountCode = code;
return true;
}
return false; // 🚫 Invalid discount code
}
// 📋 Get cart summary
getSummary(): string {
if (this.items.length === 0) {
return "🛒 Your cart is empty";
}
return `🛒 ${this.items.length} items - $${this.getTotal().toFixed(2)}`;
}
}
Now let’s write tests and see what Istanbul reveals:
// 🧪 src/cart.test.ts - Our test file
import { ShoppingCart, Product } from './cart';
describe('ShoppingCart', () => {
let cart: ShoppingCart;
const mockProduct: Product = {
id: '1',
name: 'TypeScript Book',
price: 29.99,
category: 'books',
emoji: '📘'
};
beforeEach(() => {
cart = new ShoppingCart();
});
// ✅ This test covers the addItem method
test('should add items to cart', () => {
cart.addItem(mockProduct, 2);
expect(cart.getSummary()).toBe('🛒 1 items - $59.98');
});
// ✅ This covers the getTotal method (no discount path)
test('should calculate total without discount', () => {
cart.addItem(mockProduct, 1);
expect(cart.getTotal()).toBe(29.99);
});
// ❌ Notice: We're NOT testing removeItem, discount codes, or empty cart!
// Istanbul will show these as uncovered code paths
});
🎯 Try it yourself: Run npm run test:coverage
and see which parts are covered!
🎮 Example 2: Game Score Tracker with Full Coverage
Let’s create a more comprehensive example:
// 🏆 src/game-tracker.ts
export interface PlayerScore {
playerId: string;
name: string;
score: number;
level: number;
achievements: string[];
emoji: string;
}
export class GameTracker {
private players: Map<string, PlayerScore> = new Map();
private gameActive: boolean = false;
// 🎮 Start a new game session
startGame(): void {
this.gameActive = true;
console.log('🎮 Game started!');
}
// 🛑 End the game session
endGame(): void {
this.gameActive = false;
console.log('🏁 Game ended!');
}
// 👤 Add a new player
addPlayer(name: string, emoji: string): string {
if (!this.gameActive) {
throw new Error('Cannot add players when game is not active');
}
const playerId = `player_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.players.set(playerId, {
playerId,
name,
score: 0,
level: 1,
achievements: ['🌟 Welcome to the Game!'],
emoji
});
return playerId;
}
// 🎯 Update player score
updateScore(playerId: string, points: number): boolean {
const player = this.players.get(playerId);
if (!player) {
return false; // 🚫 Player not found
}
player.score += points;
// 📈 Check for level up (every 100 points)
const newLevel = Math.floor(player.score / 100) + 1;
if (newLevel > player.level) {
player.level = newLevel;
player.achievements.push(`🏆 Reached Level ${newLevel}!`);
// 🎊 Special achievements
if (newLevel === 5) {
player.achievements.push('⭐ Rising Star!');
} else if (newLevel === 10) {
player.achievements.push('🌟 Superstar!');
}
}
return true; // ✅ Score updated
}
// 🏆 Get leaderboard
getLeaderboard(): PlayerScore[] {
return Array.from(this.players.values())
.sort((a, b) => b.score - a.score)
.slice(0, 10); // 📊 Top 10 players
}
// 📊 Get game statistics
getGameStats(): { totalPlayers: number; averageScore: number; topScore: number } {
const players = Array.from(this.players.values());
if (players.length === 0) {
return { totalPlayers: 0, averageScore: 0, topScore: 0 };
}
const totalScore = players.reduce((sum, player) => sum + player.score, 0);
const averageScore = totalScore / players.length;
const topScore = Math.max(...players.map(player => player.score));
return {
totalPlayers: players.length,
averageScore: Math.round(averageScore),
topScore
};
}
}
Comprehensive test suite with full coverage:
// 🧪 src/game-tracker.test.ts
import { GameTracker } from './game-tracker';
describe('GameTracker', () => {
let tracker: GameTracker;
beforeEach(() => {
tracker = new GameTracker();
});
describe('Game State Management', () => {
test('should start and end game', () => {
tracker.startGame();
tracker.endGame();
// ✅ Covers both startGame and endGame methods
});
test('should throw error when adding player to inactive game', () => {
// ❌ Game not started, so this should throw
expect(() => {
tracker.addPlayer('Alice', '👩');
}).toThrow('Cannot add players when game is not active');
});
});
describe('Player Management', () => {
beforeEach(() => {
tracker.startGame();
});
test('should add players successfully', () => {
const playerId = tracker.addPlayer('Alice', '👩');
expect(playerId).toMatch(/^player_/);
// ✅ Covers successful addPlayer path
});
test('should update player scores', () => {
const playerId = tracker.addPlayer('Bob', '👨');
const success = tracker.updateScore(playerId, 150);
expect(success).toBe(true);
// ✅ Covers updateScore success path and level up logic
});
test('should handle invalid player ID', () => {
const success = tracker.updateScore('invalid-id', 100);
expect(success).toBe(false);
// ✅ Covers updateScore failure path
});
test('should award special achievements', () => {
const playerId = tracker.addPlayer('Charlie', '🎭');
tracker.updateScore(playerId, 500); // Level 5
tracker.updateScore(playerId, 500); // Level 10
const leaderboard = tracker.getLeaderboard();
const player = leaderboard[0];
expect(player.achievements).toContain('⭐ Rising Star!');
expect(player.achievements).toContain('🌟 Superstar!');
// ✅ Covers special achievement branches
});
});
describe('Statistics', () => {
test('should return empty stats for no players', () => {
const stats = tracker.getGameStats();
expect(stats).toEqual({
totalPlayers: 0,
averageScore: 0,
topScore: 0
});
// ✅ Covers empty game stats branch
});
test('should calculate game statistics correctly', () => {
tracker.startGame();
const player1 = tracker.addPlayer('Alice', '👩');
const player2 = tracker.addPlayer('Bob', '👨');
tracker.updateScore(player1, 100);
tracker.updateScore(player2, 300);
const stats = tracker.getGameStats();
expect(stats.totalPlayers).toBe(2);
expect(stats.averageScore).toBe(200);
expect(stats.topScore).toBe(300);
// ✅ Covers game stats calculation logic
});
});
});
🚀 Advanced Coverage Concepts
📊 Understanding Coverage Metrics
When you run your coverage report, you’ll see four key metrics:
// 🎯 This is what Istanbul tracks in your code:
function processOrder(order: any) {
// Line 1: Statement coverage tracks this
if (order.total > 100) { // Branch coverage: condition true/false
// Line 2: Branch coverage tracks this path
return applyDiscount(order); // Function coverage: was this called?
} else {
// Line 3: Branch coverage tracks this alternative path
return order;
}
// Line 4: This would be unreachable code
}
// 🧙♂️ Istanbul generates reports showing:
// Lines: 3/4 (75%) - Line 4 never executed
// Functions: 1/2 (50%) - applyDiscount never called if total <= 100
// Branches: 1/2 (50%) - Only tested total > 100 case
// Statements: 3/4 (75%) - Same as lines in most cases
🛡️ Setting Coverage Thresholds
Configure quality gates in your .nycrc.json
:
{
"check-coverage": true,
"lines": 80,
"functions": 80,
"branches": 75,
"statements": 80,
"per-file": true,
"thresholds": {
"src/critical/*.ts": {
"lines": 95,
"functions": 95,
"branches": 90
}
}
}
🎨 Custom Reporters and Integration
Create a custom coverage script:
// 🛠️ scripts/coverage-check.ts
import { exec } from 'child_process';
import { readFileSync } from 'fs';
// 🎯 Run coverage and parse results
exec('nyc --reporter=json npm test', (error, stdout, stderr) => {
if (error) {
console.error('❌ Coverage check failed:', error);
process.exit(1);
}
try {
const coverage = JSON.parse(readFileSync('./coverage/coverage-final.json', 'utf8'));
const summary = coverage.total;
console.log('📊 Coverage Summary:');
console.log(` 📝 Lines: ${summary.lines.pct}%`);
console.log(` 🔧 Functions: ${summary.functions.pct}%`);
console.log(` 🌿 Branches: ${summary.branches.pct}%`);
console.log(` 📋 Statements: ${summary.statements.pct}%`);
// 🎉 Celebrate good coverage!
if (summary.lines.pct >= 80) {
console.log('🎉 Excellent coverage! Keep it up!');
} else {
console.log('⚠️ Coverage below threshold. Consider adding more tests.');
process.exit(1);
}
} catch (parseError) {
console.error('❌ Failed to parse coverage report:', parseError);
process.exit(1);
}
});
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Chasing 100% Coverage
// ❌ Don't write meaningless tests just for coverage!
test('should create user object', () => {
const user = new User('John', '[email protected]');
expect(user.name).toBe('John'); // 😰 This doesn't test real behavior!
});
// ✅ Write meaningful tests that verify important behavior
test('should validate email format when creating user', () => {
expect(() => {
new User('John', 'invalid-email');
}).toThrow('Invalid email format'); // 🎯 Tests actual business logic!
});
🤯 Pitfall 2: Ignoring Branch Coverage
// ❌ Only testing the happy path
function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Division by zero!');
}
return a / b;
}
// ❌ This test only covers one branch
test('should divide numbers', () => {
expect(divide(10, 2)).toBe(5);
// 😱 Never tests the division by zero case!
});
// ✅ Test all branches
test('should divide numbers correctly', () => {
expect(divide(10, 2)).toBe(5);
});
test('should throw error for division by zero', () => {
expect(() => divide(10, 0)).toThrow('Division by zero!');
// ✅ Now we have 100% branch coverage!
});
🚫 Pitfall 3: Not Excluding the Right Files
// ❌ Including too much in coverage
{
"include": ["**/*.ts"] // 😰 This includes test files, config files, etc.!
}
// ✅ Be specific about what to measure
{
"include": ["src/**/*.ts"],
"exclude": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.d.ts",
"**/test-utils/**",
"**/mocks/**"
]
}
🛠️ Best Practices
- 🎯 Focus on Important Code: Prioritize coverage for critical business logic
- 📊 Use Multiple Metrics: Don’t rely on line coverage alone - watch branches!
- 🚀 Automate Coverage Checks: Integrate coverage thresholds into your CI/CD
- 📈 Gradual Improvement: Increase coverage thresholds incrementally
- 🎨 Make Reports Accessible: Share HTML reports with your team
- ⚡ Performance Matters: Use coverage only in test environments
- 🔍 Review Uncovered Code: Manually inspect what’s not covered - is it dead code?
🧪 Hands-On Exercise
🎯 Challenge: Build a Library Management System with Full Coverage
Create a type-safe library system and achieve 100% test coverage:
📋 Requirements:
- ✅ Book management (add, remove, search)
- 👤 User management (register, borrow, return books)
- 📅 Due date tracking with overdue detection
- 🎯 Different user types (student, faculty, guest)
- 📊 Library statistics and reporting
- 🔍 Search functionality with multiple criteria
🚀 Coverage Goals:
- Lines: 95%
- Functions: 100%
- Branches: 90%
- Statements: 95%
🎨 Features to Implement:
- Error handling for all edge cases
- Input validation
- Business rule enforcement
- Comprehensive logging
💡 Solution
🔍 Click to see solution
// 📚 src/library.ts
export type UserType = 'student' | 'faculty' | 'guest';
export type BookStatus = 'available' | 'borrowed' | 'reserved';
export interface Book {
id: string;
title: string;
author: string;
isbn: string;
category: string;
status: BookStatus;
borrowedBy?: string;
dueDate?: Date;
emoji: string;
}
export interface User {
id: string;
name: string;
email: string;
type: UserType;
borrowedBooks: string[];
maxBooks: number;
emoji: string;
}
export class LibraryManager {
private books: Map<string, Book> = new Map();
private users: Map<string, User> = new Map();
// 📖 Add a new book
addBook(book: Omit<Book, 'id' | 'status'>): string {
const id = `book_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
this.books.set(id, {
...book,
id,
status: 'available'
});
return id;
}
// 👤 Register a new user
registerUser(userData: Omit<User, 'id' | 'borrowedBooks' | 'maxBooks'>): string {
if (!userData.email.includes('@')) {
throw new Error('Invalid email format');
}
const id = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
// 🎯 Set max books based on user type
let maxBooks: number;
switch (userData.type) {
case 'faculty':
maxBooks = 10;
break;
case 'student':
maxBooks = 5;
break;
case 'guest':
maxBooks = 2;
break;
default:
throw new Error('Invalid user type');
}
this.users.set(id, {
...userData,
id,
borrowedBooks: [],
maxBooks
});
return id;
}
// 📚 Borrow a book
borrowBook(userId: string, bookId: string): boolean {
const user = this.users.get(userId);
const book = this.books.get(bookId);
if (!user) {
throw new Error('User not found');
}
if (!book) {
throw new Error('Book not found');
}
if (book.status !== 'available') {
return false; // 📚 Book not available
}
if (user.borrowedBooks.length >= user.maxBooks) {
return false; // 🚫 User has reached borrowing limit
}
// 📅 Set due date based on user type
const dueDate = new Date();
switch (user.type) {
case 'faculty':
dueDate.setDate(dueDate.getDate() + 30); // 30 days
break;
case 'student':
dueDate.setDate(dueDate.getDate() + 14); // 14 days
break;
case 'guest':
dueDate.setDate(dueDate.getDate() + 7); // 7 days
break;
}
// 🔄 Update book and user records
book.status = 'borrowed';
book.borrowedBy = userId;
book.dueDate = dueDate;
user.borrowedBooks.push(bookId);
return true;
}
// 📖 Return a book
returnBook(userId: string, bookId: string): { success: boolean; wasOverdue: boolean } {
const user = this.users.get(userId);
const book = this.books.get(bookId);
if (!user || !book) {
throw new Error('User or book not found');
}
if (book.borrowedBy !== userId) {
return { success: false, wasOverdue: false };
}
const wasOverdue = book.dueDate ? new Date() > book.dueDate : false;
// 🔄 Update records
book.status = 'available';
book.borrowedBy = undefined;
book.dueDate = undefined;
user.borrowedBooks = user.borrowedBooks.filter(id => id !== bookId);
return { success: true, wasOverdue };
}
// 🔍 Search books
searchBooks(query: string, category?: string): Book[] {
const searchTerm = query.toLowerCase();
return Array.from(this.books.values()).filter(book => {
const matchesQuery =
book.title.toLowerCase().includes(searchTerm) ||
book.author.toLowerCase().includes(searchTerm) ||
book.isbn.includes(searchTerm);
const matchesCategory = !category || book.category === category;
return matchesQuery && matchesCategory;
});
}
// 📊 Get overdue books
getOverdueBooks(): Book[] {
const now = new Date();
return Array.from(this.books.values()).filter(book =>
book.status === 'borrowed' &&
book.dueDate &&
now > book.dueDate
);
}
// 📈 Get library statistics
getStats(): {
totalBooks: number;
availableBooks: number;
borrowedBooks: number;
totalUsers: number;
overdueBooks: number;
} {
const books = Array.from(this.books.values());
const overdueBooks = this.getOverdueBooks();
return {
totalBooks: books.length,
availableBooks: books.filter(b => b.status === 'available').length,
borrowedBooks: books.filter(b => b.status === 'borrowed').length,
totalUsers: this.users.size,
overdueBooks: overdueBooks.length
};
}
}
// 🧪 src/library.test.ts - Comprehensive test suite
import { LibraryManager, UserType } from './library';
describe('LibraryManager', () => {
let library: LibraryManager;
beforeEach(() => {
library = new LibraryManager();
});
describe('Book Management', () => {
test('should add books successfully', () => {
const bookId = library.addBook({
title: 'TypeScript Handbook',
author: 'Microsoft',
isbn: '978-1234567890',
category: 'Programming',
emoji: '📘'
});
expect(bookId).toMatch(/^book_/);
});
test('should search books by title, author, and ISBN', () => {
library.addBook({
title: 'TypeScript Handbook',
author: 'Microsoft',
isbn: '978-1234567890',
category: 'Programming',
emoji: '📘'
});
// Test title search
expect(library.searchBooks('typescript')).toHaveLength(1);
// Test author search
expect(library.searchBooks('microsoft')).toHaveLength(1);
// Test ISBN search
expect(library.searchBooks('978-1234567890')).toHaveLength(1);
// Test category filter
expect(library.searchBooks('', 'Programming')).toHaveLength(1);
// Test no results
expect(library.searchBooks('nonexistent')).toHaveLength(0);
});
});
describe('User Management', () => {
test('should register users with correct book limits', () => {
// Faculty user
const facultyId = library.registerUser({
name: 'Dr. Smith',
email: '[email protected]',
type: 'faculty',
emoji: '👨🏫'
});
expect(facultyId).toMatch(/^user_/);
// Student user
const studentId = library.registerUser({
name: 'Alice',
email: '[email protected]',
type: 'student',
emoji: '👩🎓'
});
expect(studentId).toMatch(/^user_/);
// Guest user
const guestId = library.registerUser({
name: 'Visitor',
email: '[email protected]',
type: 'guest',
emoji: '👤'
});
expect(guestId).toMatch(/^user_/);
});
test('should validate email format', () => {
expect(() => {
library.registerUser({
name: 'Invalid User',
email: 'invalid-email',
type: 'student',
emoji: '❌'
});
}).toThrow('Invalid email format');
});
test('should reject invalid user types', () => {
expect(() => {
library.registerUser({
name: 'Invalid User',
email: '[email protected]',
type: 'invalid' as UserType,
emoji: '❌'
});
}).toThrow('Invalid user type');
});
});
describe('Borrowing System', () => {
let bookId: string;
let facultyId: string;
let studentId: string;
let guestId: string;
beforeEach(() => {
bookId = library.addBook({
title: 'Test Book',
author: 'Test Author',
isbn: '123456789',
category: 'Test',
emoji: '📖'
});
facultyId = library.registerUser({
name: 'Faculty',
email: '[email protected]',
type: 'faculty',
emoji: '👨🏫'
});
studentId = library.registerUser({
name: 'Student',
email: '[email protected]',
type: 'student',
emoji: '👩🎓'
});
guestId = library.registerUser({
name: 'Guest',
email: '[email protected]',
type: 'guest',
emoji: '👤'
});
});
test('should allow borrowing available books', () => {
const success = library.borrowBook(studentId, bookId);
expect(success).toBe(true);
});
test('should prevent borrowing unavailable books', () => {
library.borrowBook(studentId, bookId); // First borrow
const secondBorrow = library.borrowBook(facultyId, bookId);
expect(secondBorrow).toBe(false);
});
test('should enforce borrowing limits', () => {
// Create multiple books for testing limits
const books = Array.from({ length: 6 }, (_, i) =>
library.addBook({
title: `Book ${i}`,
author: 'Author',
isbn: `12345678${i}`,
category: 'Test',
emoji: '📖'
})
);
// Student can borrow 5 books
books.slice(0, 5).forEach(id => {
expect(library.borrowBook(studentId, id)).toBe(true);
});
// 6th book should fail
expect(library.borrowBook(studentId, books[5])).toBe(false);
});
test('should handle invalid user/book IDs', () => {
expect(() => {
library.borrowBook('invalid-user', bookId);
}).toThrow('User not found');
expect(() => {
library.borrowBook(studentId, 'invalid-book');
}).toThrow('Book not found');
});
test('should return books successfully', () => {
library.borrowBook(studentId, bookId);
const result = library.returnBook(studentId, bookId);
expect(result.success).toBe(true);
expect(result.wasOverdue).toBe(false);
});
test('should detect overdue returns', () => {
// This test would require mocking Date to simulate overdue
library.borrowBook(guestId, bookId);
// Simulate returning after due date
const result = library.returnBook(guestId, bookId);
expect(result.success).toBe(true);
// In real implementation, you'd mock the date to test overdue
});
test('should prevent returning books not borrowed by user', () => {
library.borrowBook(studentId, bookId);
const result = library.returnBook(facultyId, bookId);
expect(result.success).toBe(false);
});
test('should handle invalid return requests', () => {
expect(() => {
library.returnBook('invalid-user', bookId);
}).toThrow('User or book not found');
expect(() => {
library.returnBook(studentId, 'invalid-book');
}).toThrow('User or book not found');
});
});
describe('Statistics and Reporting', () => {
test('should provide accurate library statistics', () => {
// Add some test data
const book1 = library.addBook({
title: 'Book 1',
author: 'Author 1',
isbn: '111',
category: 'Fiction',
emoji: '📚'
});
const book2 = library.addBook({
title: 'Book 2',
author: 'Author 2',
isbn: '222',
category: 'Non-fiction',
emoji: '📖'
});
const userId = library.registerUser({
name: 'Test User',
email: '[email protected]',
type: 'student',
emoji: '👤'
});
library.borrowBook(userId, book1);
const stats = library.getStats();
expect(stats.totalBooks).toBe(2);
expect(stats.availableBooks).toBe(1);
expect(stats.borrowedBooks).toBe(1);
expect(stats.totalUsers).toBe(1);
expect(stats.overdueBooks).toBe(0);
});
test('should identify overdue books', () => {
const bookId = library.addBook({
title: 'Test Book',
author: 'Author',
isbn: '123',
category: 'Test',
emoji: '📖'
});
const userId = library.registerUser({
name: 'User',
email: '[email protected]',
type: 'guest',
emoji: '👤'
});
library.borrowBook(userId, bookId);
// In a real test, you'd mock Date to simulate overdue books
const overdueBooks = library.getOverdueBooks();
expect(Array.isArray(overdueBooks)).toBe(true);
});
});
});
🎓 Key Takeaways
You’ve learned so much about code coverage with Istanbul! Here’s what you can now do:
- ✅ Set up Istanbul in TypeScript projects with confidence 💪
- ✅ Generate beautiful reports in multiple formats 📊
- ✅ Interpret coverage metrics like lines, branches, and functions 🔍
- ✅ Configure quality thresholds to maintain code quality 🛡️
- ✅ Debug uncovered code paths and write meaningful tests 🐛
- ✅ Integrate coverage into your CI/CD pipeline 🚀
Remember: Coverage is a tool, not a goal! Focus on testing meaningful behavior rather than just hitting arbitrary numbers. 🎯
🤝 Next Steps
Congratulations! 🎉 You’ve mastered Istanbul code coverage!
Here’s what to do next:
- 💻 Set up coverage in your current TypeScript project
- 🎯 Establish reasonable coverage thresholds for your team
- 📊 Generate your first coverage report and explore the HTML output
- 🏗️ Integrate coverage checks into your build process
- 📚 Move on to our next tutorial: Advanced Testing Patterns
- 🌟 Share your coverage achievements with your team!
Remember: Every line of well-tested code is a step toward more reliable software. Keep testing, keep improving, and most importantly, have fun building robust applications! 🚀
Happy testing! 🎉🧪✨