Prerequisites
- Basic understanding of JavaScript 📝
- TypeScript installation ⚡
- VS Code or preferred IDE 💻
What you'll learn
- Understand continuous testing fundamentals 🎯
- Apply CI/CD testing in real projects 🏗️
- Debug common CI/CD issues 🐛
- Write type-safe testing pipelines ✨
🎯 Introduction
Welcome to the exciting world of continuous testing and CI/CD integration! 🎉 In this guide, we’ll explore how to set up automated testing pipelines that run every time you push code.
You’ll discover how continuous testing can transform your TypeScript development workflow. Whether you’re building web applications 🌐, server-side code 🖥️, or libraries 📚, understanding CI/CD testing is essential for maintaining code quality and catching bugs before they reach production.
By the end of this tutorial, you’ll feel confident setting up robust testing pipelines in your own projects! Let’s dive in! 🏊♂️
📚 Understanding Continuous Testing
🤔 What is Continuous Testing?
Continuous testing is like having a vigilant security guard 🛡️ for your code! Think of it as an automated quality assurance team that never sleeps, constantly checking your code every time you make changes.
In TypeScript terms, continuous testing means automatically running your tests, type checks, and quality gates whenever code is pushed to your repository 🚀. This means you can:
- ✨ Catch bugs before they reach production
- 🚀 Deploy with confidence
- 🛡️ Maintain consistent code quality
- 📊 Get instant feedback on your changes
💡 Why Use CI/CD Testing?
Here’s why developers love continuous testing:
- Early Bug Detection 🔍: Catch issues when they’re cheapest to fix
- Team Collaboration 👥: Everyone’s changes are validated automatically
- Deployment Confidence 🚀: Know your code works before it goes live
- Quality Consistency 📏: Maintain standards across all team members
Real-world example: Imagine building an e-commerce platform 🛒. With continuous testing, you can ensure that every shopping cart feature works perfectly before customers see it!
🔧 Basic CI/CD Setup
📝 GitHub Actions Configuration
Let’s start with a friendly GitHub Actions workflow:
# 🚀 .github/workflows/test.yml
name: 🧪 TypeScript Tests
on:
push:
branches: [ main, develop ] # 🌿 Run on main branches
pull_request:
branches: [ main ] # 🔄 Test PRs too
jobs:
test:
runs-on: ubuntu-latest # 🐧 Use Linux runner
strategy:
matrix:
node-version: [18, 20] # 📦 Test multiple Node versions
steps:
# 📥 Checkout the code
- name: 📥 Checkout code
uses: actions/checkout@v4
# ⚡ Setup Node.js
- name: ⚡ Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm' # 🚀 Cache dependencies
# 📦 Install dependencies
- name: 📦 Install dependencies
run: npm ci
# 🔍 Run TypeScript checks
- name: 🔍 Type check
run: npm run typecheck
# 🧪 Run tests
- name: 🧪 Run tests
run: npm run test:ci
# 📊 Upload coverage
- name: 📊 Upload coverage
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
💡 Explanation: This workflow runs on every push and pull request, testing multiple Node.js versions to ensure compatibility!
🎯 Package.json Scripts
Here are the scripts you’ll need:
{
"scripts": {
"test": "jest",
"test:ci": "jest --ci --coverage --watchAll=false",
"test:watch": "jest --watch",
"typecheck": "tsc --noEmit",
"lint": "eslint src/**/*.{ts,tsx}",
"lint:fix": "eslint src/**/*.{ts,tsx} --fix",
"build": "tsc && npm run test:ci"
}
}
💡 Practical Examples
🛒 Example 1: E-commerce Testing Pipeline
Let’s build a complete testing setup for an online store:
// 🧪 tests/setup.ts - Test configuration
import { jest } from '@jest/globals';
// 🌍 Global test setup
beforeAll(() => {
console.log('🚀 Starting test suite...');
});
afterAll(() => {
console.log('✅ All tests completed!');
});
// 🛠️ Mock external services
jest.mock('../src/services/paymentService', () => ({
processPayment: jest.fn(() => Promise.resolve({ success: true, id: 'pay_123' }))
}));
// 🛒 src/models/ShoppingCart.ts
export interface Product {
id: string;
name: string;
price: number;
emoji: string;
}
export class ShoppingCart {
private items: Product[] = [];
// ➕ Add item to cart
addItem(product: Product): void {
if (product.price <= 0) {
throw new Error('❌ Product price must be positive');
}
this.items.push(product);
}
// 💰 Calculate total
getTotal(): number {
return this.items.reduce((sum, item) => sum + item.price, 0);
}
// 📋 Get item count
getItemCount(): number {
return this.items.length;
}
// 🗑️ Clear cart
clear(): void {
this.items = [];
}
}
// 🧪 tests/ShoppingCart.test.ts
import { ShoppingCart, Product } from '../src/models/ShoppingCart';
describe('🛒 Shopping Cart Tests', () => {
let cart: ShoppingCart;
beforeEach(() => {
cart = new ShoppingCart(); // 🆕 Fresh cart for each test
});
test('✅ should add items correctly', () => {
// 🎯 Arrange
const product: Product = {
id: '1',
name: 'TypeScript Book',
price: 29.99,
emoji: '📘'
};
// 🎬 Act
cart.addItem(product);
// 🔍 Assert
expect(cart.getItemCount()).toBe(1);
expect(cart.getTotal()).toBe(29.99);
});
test('🚫 should reject negative prices', () => {
// 🎯 Arrange
const invalidProduct: Product = {
id: '2',
name: 'Free Item',
price: -5,
emoji: '💸'
};
// 🎬 Act & Assert
expect(() => cart.addItem(invalidProduct))
.toThrow('❌ Product price must be positive');
});
test('🧮 should calculate total correctly', () => {
// 🎯 Arrange
const products: Product[] = [
{ id: '1', name: 'Coffee', price: 4.99, emoji: '☕' },
{ id: '2', name: 'Donut', price: 2.50, emoji: '🍩' }
];
// 🎬 Act
products.forEach(p => cart.addItem(p));
// 🔍 Assert
expect(cart.getTotal()).toBe(7.49);
});
});
🎮 Example 2: Game Testing with Mock APIs
Let’s test a game with external API calls:
// 🎮 src/services/GameService.ts
import { LeaderboardEntry } from './types';
export class GameService {
private apiUrl = process.env.GAME_API_URL || 'https://api.game.com';
// 🏆 Submit high score
async submitScore(playerName: string, score: number): Promise<boolean> {
try {
const response = await fetch(`${this.apiUrl}/scores`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ playerName, score })
});
return response.ok;
} catch (error) {
console.error('🚨 Failed to submit score:', error);
return false;
}
}
// 📊 Get leaderboard
async getLeaderboard(): Promise<LeaderboardEntry[]> {
try {
const response = await fetch(`${this.apiUrl}/leaderboard`);
if (!response.ok) return [];
return await response.json();
} catch (error) {
console.error('🚨 Failed to fetch leaderboard:', error);
return [];
}
}
}
// 🧪 tests/GameService.test.ts
import { GameService } from '../src/services/GameService';
// 🎭 Mock fetch globally
global.fetch = jest.fn();
const mockFetch = fetch as jest.MockedFunction<typeof fetch>;
describe('🎮 Game Service Tests', () => {
let gameService: GameService;
beforeEach(() => {
gameService = new GameService();
mockFetch.mockClear(); // 🧹 Clean slate for each test
});
test('🏆 should submit score successfully', async () => {
// 🎯 Arrange
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200
} as Response);
// 🎬 Act
const result = await gameService.submitScore('Player1', 1000);
// 🔍 Assert
expect(result).toBe(true);
expect(mockFetch).toHaveBeenCalledWith(
expect.stringContaining('/scores'),
expect.objectContaining({
method: 'POST',
body: JSON.stringify({ playerName: 'Player1', score: 1000 })
})
);
});
test('🚨 should handle API errors gracefully', async () => {
// 🎯 Arrange
mockFetch.mockRejectedValueOnce(new Error('Network error'));
// 🎬 Act
const result = await gameService.submitScore('Player1', 1000);
// 🔍 Assert
expect(result).toBe(false);
});
});
🚀 Advanced CI/CD Concepts
🧙♂️ Multi-Environment Testing
When you’re ready to level up, try testing across multiple environments:
# 🌍 .github/workflows/multi-env.yml
name: 🌍 Multi-Environment Tests
on:
push:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [development, staging, production]
include:
- environment: development
api_url: "https://dev-api.example.com"
- environment: staging
api_url: "https://staging-api.example.com"
- environment: production
api_url: "https://api.example.com"
steps:
- uses: actions/checkout@v4
- name: 🔧 Setup Environment
run: |
echo "API_URL=${{ matrix.api_url }}" >> $GITHUB_ENV
echo "ENVIRONMENT=${{ matrix.environment }}" >> $GITHUB_ENV
- name: 🧪 Run Environment Tests
run: npm run test:${{ matrix.environment }}
🏗️ Parallel Testing Strategy
For larger projects, run tests in parallel:
// 🧪 jest.config.js
module.exports = {
// 🚀 Run tests in parallel
maxWorkers: '50%',
// 📊 Coverage thresholds
coverageThreshold: {
global: {
branches: 80, // 🌿 80% branch coverage
functions: 80, // ⚡ 80% function coverage
lines: 80, // 📝 80% line coverage
statements: 80 // 💬 80% statement coverage
}
},
// 🎯 Test file patterns
testMatch: [
'<rootDir>/tests/**/*.test.ts',
'<rootDir>/src/**/__tests__/*.test.ts'
],
// 🛡️ Setup files
setupFilesAfterEnv: ['<rootDir>/tests/setup.ts'],
// 📊 Reporters
reporters: [
'default',
['jest-junit', { outputDirectory: 'test-results' }]
]
};
⚠️ Common Pitfalls and Solutions
😱 Pitfall 1: Flaky Tests
// ❌ Wrong way - timing-dependent test
test('should update after delay', async () => {
startAsyncOperation();
await new Promise(resolve => setTimeout(resolve, 100)); // 💥 Unreliable!
expect(getResult()).toBe('updated');
});
// ✅ Correct way - wait for actual condition
test('should update after delay', async () => {
startAsyncOperation();
await waitFor(() => {
expect(getResult()).toBe('updated');
}, { timeout: 5000 }); // ✅ Wait for real completion!
});
🤯 Pitfall 2: Environment-Specific Issues
// ❌ Dangerous - hardcoded paths
const configPath = '/home/user/config.json'; // 💥 Won't work in CI!
// ✅ Safe - use environment variables
const configPath = process.env.CONFIG_PATH || './config.json'; // ✅ Flexible!
// 🛡️ Better - cross-platform paths
import path from 'path';
const configPath = path.join(process.cwd(), 'config.json');
🚨 Pitfall 3: Ignoring Exit Codes
# ❌ Wrong - ignoring failures
- name: Run tests
run: npm test || echo "Tests failed but continuing..."
# ✅ Correct - fail fast on test failures
- name: Run tests
run: npm test
# 🛡️ Alternative - conditional steps
- name: Run tests
run: npm test
continue-on-error: false
🛠️ Best Practices
- 🎯 Test Early and Often: Run tests on every commit, not just releases
- 📝 Clear Test Names: Make failures easy to understand
- 🛡️ Environment Parity: CI should match production as closely as possible
- 🚀 Fast Feedback: Keep test suites under 5 minutes when possible
- ✨ Reliable Tests: Eliminate flaky tests that randomly fail
- 📊 Monitor Metrics: Track test coverage and performance trends
🧪 Hands-On Exercise
🎯 Challenge: Build a Complete CI/CD Pipeline
Create a full testing pipeline for a TypeScript project:
📋 Requirements:
- ✅ Unit tests with Jest
- 🔍 TypeScript type checking
- 🎨 ESLint code quality checks
- 📊 Code coverage reporting
- 🚀 Multi-environment testing
- 🏷️ Automated semantic versioning
- 📦 Build and artifact generation
🚀 Bonus Points:
- Add integration tests
- Implement security scanning
- Set up performance benchmarks
- Create deployment automation
💡 Solution
🔍 Click to see solution
# 🎯 Complete CI/CD pipeline
name: 🚀 Complete TypeScript CI/CD
on:
push:
branches: [ main, develop, 'feature/*' ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '20'
jobs:
# 🔍 Code Quality Checks
quality:
name: 🎨 Code Quality
runs-on: ubuntu-latest
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
- name: ⚡ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: 📦 Install dependencies
run: npm ci
- name: 🔍 Type check
run: npm run typecheck
- name: 🎨 Lint check
run: npm run lint
- name: 🔒 Security audit
run: npm audit --audit-level moderate
# 🧪 Testing
test:
name: 🧪 Tests
runs-on: ubuntu-latest
needs: quality
strategy:
matrix:
node-version: [18, 20]
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
- name: ⚡ Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: 📦 Install dependencies
run: npm ci
- name: 🧪 Run unit tests
run: npm run test:ci
- name: 📊 Upload coverage
uses: codecov/codecov-action@v3
if: matrix.node-version == 20
with:
file: ./coverage/lcov.info
fail_ci_if_error: true
# 🏗️ Build
build:
name: 🏗️ Build
runs-on: ubuntu-latest
needs: [quality, test]
steps:
- name: 📥 Checkout
uses: actions/checkout@v4
- name: ⚡ Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: 📦 Install dependencies
run: npm ci
- name: 🏗️ Build project
run: npm run build
- name: 📦 Archive artifacts
uses: actions/upload-artifact@v4
with:
name: build-files
path: dist/
# 🚀 Deploy (on main branch only)
deploy:
name: 🚀 Deploy
runs-on: ubuntu-latest
needs: build
if: github.ref == 'refs/heads/main'
steps:
- name: 📥 Download artifacts
uses: actions/download-artifact@v4
with:
name: build-files
path: dist/
- name: 🚀 Deploy to staging
run: echo "🎉 Deploying to staging environment!"
- name: 🧪 Run integration tests
run: echo "🔍 Running integration tests..."
- name: ✅ Deploy to production
run: echo "🎊 Deploying to production!"
// 🧪 Complete test setup
import { setupServer } from 'msw/node';
import { rest } from 'msw';
// 🎭 Setup mock server for integration tests
const server = setupServer(
rest.get('/api/health', (req, res, ctx) => {
return res(ctx.json({ status: 'healthy', emoji: '✅' }));
}),
rest.post('/api/users', (req, res, ctx) => {
return res(ctx.json({ id: '123', created: true, emoji: '🎉' }));
})
);
// 🛠️ Test lifecycle
beforeAll(() => {
server.listen();
console.log('🎭 Mock server started for tests');
});
afterEach(() => {
server.resetHandlers();
});
afterAll(() => {
server.close();
console.log('✅ Mock server stopped');
});
🎓 Key Takeaways
You’ve learned so much! Here’s what you can now do:
- ✅ Set up CI/CD pipelines with confidence 💪
- ✅ Avoid common testing mistakes that trip up teams 🛡️
- ✅ Apply best practices in real projects 🎯
- ✅ Debug CI/CD issues like a pro 🐛
- ✅ Build robust testing workflows with TypeScript! 🚀
Remember: Continuous testing is your safety net, not a burden! It’s there to help you deploy with confidence. 🤝
🤝 Next Steps
Congratulations! 🎉 You’ve mastered continuous testing and CI/CD integration!
Here’s what to do next:
- 💻 Set up a CI/CD pipeline for your current project
- 🏗️ Add comprehensive test coverage to an existing app
- 📚 Move on to our next tutorial: Advanced Testing Patterns
- 🌟 Share your CI/CD setup with your team!
Remember: Every reliable system was built with good testing practices. Keep coding, keep testing, and most importantly, deploy with confidence! 🚀
Happy testing! 🎉🚀✨