+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 225 of 355

🧪 Mutation Testing: Stryker Mutator

Master mutation testing: stryker mutator in TypeScript with practical examples, best practices, and real-world applications 🚀

🚀Intermediate
30 min read

Prerequisites

  • Basic understanding of JavaScript 📝
  • TypeScript installation ⚡
  • VS Code or preferred IDE 💻
  • Jest or Vitest testing framework 🧪

What you'll learn

  • Understand mutation testing fundamentals 🎯
  • Apply Stryker mutator in real projects 🏗️
  • Debug common mutation testing issues 🐛
  • Write type-safe mutation tests ✨

🎯 Introduction

Welcome to the fascinating world of mutation testing! 🎉 In this guide, we’ll explore how to test your tests with Stryker Mutator - the ultimate tool for ensuring your test suite is bulletproof! 🛡️

You’ll discover how mutation testing can reveal hidden weaknesses in your test suite and make your TypeScript applications rock-solid. Whether you’re building web apps 🌐, APIs 🖥️, or libraries 📚, understanding mutation testing is essential for writing truly robust, reliable code.

By the end of this tutorial, you’ll feel confident using Stryker to supercharge your testing strategy! Let’s dive in! 🏊‍♂️

📚 Understanding Mutation Testing

🤔 What is Mutation Testing?

Mutation testing is like having an evil twin for your code! 😈 Think of it as a mischievous gremlin that deliberately breaks your code in small ways to see if your tests catch the problems.

In TypeScript terms, mutation testing introduces small changes (mutations) to your source code and runs your tests to see if they fail 🎯. This process helps you discover:

  • ✨ Weak spots in your test coverage
  • 🚀 Missing edge cases in your tests
  • 🛡️ False confidence from passing but ineffective tests

💡 Why Use Mutation Testing?

Here’s why developers love mutation testing:

  1. Test Quality Assurance 🔒: Ensures tests actually test something meaningful
  2. Hidden Bug Detection 💻: Reveals untested code paths
  3. Confidence Building 📖: Proves your tests are effective
  4. Quality Metrics 🔧: Provides mutation score as quality indicator

Real-world example: Imagine building a shopping cart 🛒. Regular tests might check if items are added, but mutation testing would verify that your tests catch when the wrong price is calculated!

🔧 Basic Syntax and Usage

📝 Installation and Setup

Let’s start with setting up Stryker:

# 👋 Install Stryker for TypeScript!
npm install --save-dev @stryker-mutator/core
npm install --save-dev @stryker-mutator/typescript-checker
npm install --save-dev @stryker-mutator/jest-runner

# 🎨 Initialize Stryker configuration
npx stryker init

💡 Explanation: Stryker needs specific plugins for TypeScript and your test runner. The init command creates a configuration file for your project!

🎯 Basic Configuration

Here’s a typical Stryker configuration:

// 🏗️ stryker.conf.mjs - Your mutation testing config
export default {
  // 📦 Specify which files to mutate
  mutate: [
    'src/**/*.ts',
    '!src/**/*.test.ts',
    '!src/**/*.spec.ts'
  ],
  
  // 🎨 TypeScript checker for type safety
  checkers: ['typescript'],
  
  // 🧪 Test runner configuration  
  testRunner: 'jest',
  
  // 📊 Coverage thresholds
  thresholds: {
    high: 80,    // 🎯 80% is excellent!
    low: 60,     // ⚠️ Below 60% needs work
    break: 50    // 💥 Below 50% fails the build
  }
};

💡 Practical Examples

🛒 Example 1: E-commerce Calculator

Let’s test a shopping cart calculator:

// 🛍️ ShoppingCart.ts - Our code to test
export class ShoppingCart {
  private items: Array<{ price: number; quantity: number }> = [];
  
  // ➕ Add item to cart
  addItem(price: number, quantity: number): void {
    this.items.push({ price, quantity });
  }
  
  // 💰 Calculate total with tax
  calculateTotal(taxRate: number = 0.1): number {
    const subtotal = this.items.reduce((sum, item) => {
      return sum + (item.price * item.quantity);
    }, 0);
    
    return subtotal * (1 + taxRate); // 🎯 This is what we'll mutate!
  }
  
  // 📊 Get item count
  getItemCount(): number {
    return this.items.length;
  }
}
// 🧪 ShoppingCart.test.ts - Our test suite
import { ShoppingCart } from './ShoppingCart';

describe('Shopping Cart 🛒', () => {
  let cart: ShoppingCart;
  
  beforeEach(() => {
    cart = new ShoppingCart();
  });
  
  test('should calculate total correctly 💰', () => {
    cart.addItem(10, 2);  // $20 subtotal
    cart.addItem(5, 1);   // $5 subtotal
    
    const total = cart.calculateTotal(0.1); // 10% tax
    expect(total).toBe(27.5); // (20 + 5) * 1.1 = 27.5
  });
  
  test('should handle zero tax rate 🆓', () => {
    cart.addItem(10, 1);
    const total = cart.calculateTotal(0);
    expect(total).toBe(10);
  });
});

🎯 Try it yourself: Add tests for empty cart and negative quantities!

🎮 Example 2: Game Score System

Let’s build a mutation-tested score system:

// 🏆 ScoreCalculator.ts
export class ScoreCalculator {
  private baseScore: number = 0;
  private multiplier: number = 1;
  
  // 🎯 Add points with combo multiplier
  addPoints(points: number, isCombo: boolean = false): number {
    if (points < 0) {
      return this.baseScore; // 🛡️ No negative points allowed
    }
    
    const earnedPoints = isCombo ? points * 2 : points;
    this.baseScore += earnedPoints * this.multiplier;
    
    return this.baseScore;
  }
  
  // ⚡ Increase multiplier
  increaseMultiplier(): void {
    if (this.multiplier < 5) { // 🎯 Max multiplier is 5
      this.multiplier++;
    }
  }
  
  // 🔄 Reset score
  reset(): void {
    this.baseScore = 0;
    this.multiplier = 1;
  }
}
// 🧪 ScoreCalculator.test.ts
import { ScoreCalculator } from './ScoreCalculator';

describe('Score Calculator 🎮', () => {
  let calculator: ScoreCalculator;
  
  beforeEach(() => {
    calculator = new ScoreCalculator();
  });
  
  test('should add regular points correctly ✨', () => {
    const score = calculator.addPoints(100);
    expect(score).toBe(100);
  });
  
  test('should double combo points 🎊', () => {
    const score = calculator.addPoints(50, true);
    expect(score).toBe(100); // 50 * 2 = 100
  });
  
  test('should apply multiplier 🚀', () => {
    calculator.increaseMultiplier(); // multiplier = 2
    const score = calculator.addPoints(50);
    expect(score).toBe(100); // 50 * 2 = 100
  });
  
  test('should reject negative points 🛡️', () => {
    calculator.addPoints(50);
    const score = calculator.addPoints(-10);
    expect(score).toBe(50); // Should stay at 50
  });
});

🚀 Advanced Concepts

🧙‍♂️ Advanced Topic 1: Custom Mutators

When you’re ready to level up, configure custom mutators:

// 🎯 Advanced Stryker configuration
export default {
  mutate: ['src/**/*.ts'],
  
  // 🪄 Configure specific mutators
  mutator: {
    plugins: ['@stryker-mutator/typescript-checker'],
    excludedMutations: [
      'StringLiteral', // 📝 Don't mutate string literals
      'ArrayDeclaration' // 🚫 Skip array mutations
    ]
  },
  
  // 🎨 Custom mutation patterns
  coverageAnalysis: 'perTest',
  
  // 📊 Detailed reporting
  reporters: [
    'progress',
    'clear-text',
    'html',
    'json'
  ]
};

🏗️ Advanced Topic 2: Mutation Testing CI/CD

For the brave developers who want automated mutation testing:

// 🚀 package.json scripts for CI/CD
{
  "scripts": {
    "test": "jest",
    "test:mutation": "stryker run",
    "test:mutation:ci": "stryker run --concurrency 2",
    "test:mutation:fast": "stryker run --mutate src/critical/**/*.ts"
  }
}
# 🎯 GitHub Actions workflow
name: Mutation Testing 🧬
on: [push, pull_request]

jobs:
  mutation-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Node.js 📦
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      
      - name: Install dependencies 📥
        run: npm ci
      
      - name: Run mutation tests 🧪
        run: npm run test:mutation:ci

⚠️ Common Pitfalls and Solutions

😱 Pitfall 1: Testing Everything

// ❌ Wrong way - mutation testing everything!
export default {
  mutate: [
    'src/**/*.ts',
    'node_modules/**/*.ts', // 💥 This will take forever!
    'dist/**/*.ts'
  ]
};

// ✅ Correct way - be selective!
export default {
  mutate: [
    'src/**/*.ts',
    '!src/**/*.test.ts',    // 🚫 Don't mutate tests
    '!src/**/types.ts',     // 🚫 Skip type definitions
    '!src/config/**/*.ts'   // 🚫 Skip configuration
  ]
};

🤯 Pitfall 2: Ignoring Equivalent Mutants

// ❌ Dangerous - not handling equivalent mutants!
const isEven = (n: number): boolean => {
  return n % 2 === 0; // Stryker might mutate to !== 0
};

// ✅ Better test - catches the mutation!
test('should identify even numbers correctly 🎯', () => {
  expect(isEven(2)).toBe(true);
  expect(isEven(3)).toBe(false);  // 🎯 This catches the !== mutation!
  expect(isEven(4)).toBe(true);
  expect(isEven(1)).toBe(false);
});

🔥 Pitfall 3: Slow Mutation Testing

// ❌ Slow configuration
export default {
  concurrency: 1,        // 🐌 Only one mutation at a time
  timeoutMS: 30000,      // ⏰ Too generous timeout
  coverageAnalysis: 'all' // 📊 Analyzing everything
};

// ✅ Fast configuration
export default {
  concurrency: 4,           // 🚀 Parallel mutations
  timeoutMS: 5000,          // ⚡ Reasonable timeout
  coverageAnalysis: 'perTest', // 🎯 Smart analysis
  disableTypeChecks: false  // 🛡️ Keep type safety
};

🛠️ Best Practices

  1. 🎯 Start Small: Begin with critical business logic, not everything!
  2. 📝 Focus on Logic: Don’t mutate type definitions or configs
  3. 🛡️ Set Thresholds: Use reasonable mutation score targets (70-85%)
  4. 🎨 Use Fast Feedback: Run mutation tests on changed files only
  5. ✨ Monitor Performance: Keep mutation test runs under 10 minutes

🧪 Hands-On Exercise

🎯 Challenge: Build a Mutation-Tested Password Validator

Create a password validator with bulletproof tests:

📋 Requirements:

  • ✅ Check minimum length (8 characters)
  • 🔒 Require uppercase and lowercase letters
  • 🔢 Require at least one number
  • 🎨 Require at least one special character
  • 🚫 Reject common passwords
  • 📊 Return validation score (0-100)

🚀 Bonus Points:

  • Add mutation testing configuration
  • Achieve 80%+ mutation score
  • Handle edge cases (empty, null, very long passwords)

💡 Solution

🔍 Click to see solution
// 🔐 PasswordValidator.ts - Our bulletproof validator!
export class PasswordValidator {
  private commonPasswords = ['123456', 'password', 'qwerty', 'admin'];
  
  // 🎯 Validate password strength
  validatePassword(password: string): { 
    isValid: boolean; 
    score: number; 
    errors: string[] 
  } {
    if (!password) {
      return { isValid: false, score: 0, errors: ['Password is required 📝'] };
    }
    
    const errors: string[] = [];
    let score = 0;
    
    // 📏 Check length
    if (password.length < 8) {
      errors.push('Password must be at least 8 characters 📏');
    } else {
      score += 25; // 🎯 25 points for length
    }
    
    // 🔤 Check uppercase
    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain uppercase letters 🔤');
    } else {
      score += 20; // 🎯 20 points for uppercase
    }
    
    // 🔡 Check lowercase
    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain lowercase letters 🔡');
    } else {
      score += 20; // 🎯 20 points for lowercase
    }
    
    // 🔢 Check numbers
    if (!/\d/.test(password)) {
      errors.push('Password must contain numbers 🔢');
    } else {
      score += 20; // 🎯 20 points for numbers
    }
    
    // 🎨 Check special characters
    if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
      errors.push('Password must contain special characters 🎨');
    } else {
      score += 15; // 🎯 15 points for special chars
    }
    
    // 🚫 Check common passwords
    if (this.commonPasswords.includes(password.toLowerCase())) {
      errors.push('Password is too common 🚫');
      score = Math.max(0, score - 50); // 💥 Heavy penalty
    }
    
    return {
      isValid: errors.length === 0,
      score: Math.min(100, score),
      errors
    };
  }
}

// 🧪 PasswordValidator.test.ts - Comprehensive tests
import { PasswordValidator } from './PasswordValidator';

describe('Password Validator 🔐', () => {
  let validator: PasswordValidator;
  
  beforeEach(() => {
    validator = new PasswordValidator();
  });
  
  test('should accept strong password ✅', () => {
    const result = validator.validatePassword('StrongP@ss123');
    expect(result.isValid).toBe(true);
    expect(result.score).toBeGreaterThan(80);
    expect(result.errors).toHaveLength(0);
  });
  
  test('should reject short password 📏', () => {
    const result = validator.validatePassword('Sh0rt!');
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Password must be at least 8 characters 📏');
  });
  
  test('should reject password without uppercase 🔤', () => {
    const result = validator.validatePassword('lowercase123!');
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Password must contain uppercase letters 🔤');
  });
  
  test('should reject common passwords 🚫', () => {
    const result = validator.validatePassword('password');
    expect(result.isValid).toBe(false);
    expect(result.errors).toContain('Password is too common 🚫');
    expect(result.score).toBeLessThan(50);
  });
  
  test('should handle empty password 📝', () => {
    const result = validator.validatePassword('');
    expect(result.isValid).toBe(false);
    expect(result.score).toBe(0);
    expect(result.errors).toContain('Password is required 📝');
  });
  
  test('should handle null password 🚫', () => {
    const result = validator.validatePassword(null as any);
    expect(result.isValid).toBe(false);
    expect(result.score).toBe(0);
  });
});

// 🎯 stryker.conf.mjs - Mutation testing config
export default {
  mutate: [
    'src/PasswordValidator.ts' // 🎯 Target our validator
  ],
  testRunner: 'jest',
  coverageAnalysis: 'perTest',
  thresholds: {
    high: 85,  // 🎯 High standards!
    low: 70,
    break: 60
  },
  reporters: ['progress', 'clear-text', 'html']
};

🎓 Key Takeaways

You’ve learned so much! Here’s what you can now do:

  • Set up Stryker Mutator with confidence 💪
  • Configure mutation testing for TypeScript projects 🛡️
  • Interpret mutation scores and improve test quality 🎯
  • Debug mutation failures like a pro 🐛
  • Build bulletproof test suites that catch real bugs! 🚀

Remember: Mutation testing is like having a sparring partner for your tests - it makes them stronger! 🥊

🤝 Next Steps

Congratulations! 🎉 You’ve mastered mutation testing with Stryker!

Here’s what to do next:

  1. 💻 Practice with the password validator exercise
  2. 🏗️ Add mutation testing to your current project
  3. 📚 Move on to our next tutorial: Property-Based Testing
  4. 🌟 Share your mutation scores with your team!

Remember: Every bug caught by mutation testing is a bug that won’t reach production. Keep testing, keep mutating, and most importantly, keep building awesome software! 🚀


Happy mutation testing! 🎉🧪✨