Testing Types and Methodologies

Module 27: Testing & Quality Assurance

Introduction

In our previous lecture, we explored the fundamental principles of software testing. Now, we'll delve deeper into the various types of testing methodologies that form the backbone of a comprehensive testing strategy.

Understanding these different testing approaches helps you select the right techniques for your specific project needs, ensuring that your software is thoroughly validated from multiple perspectives.

mindmap root((Testing Types)) Functional Unit Integration System Acceptance Non-Functional Performance Security Usability Accessibility Compatibility By Stage Alpha Beta Regression By Approach Static Dynamic White Box Black Box Gray Box

Functional Testing

Functional testing focuses on validating that the software behaves according to specified requirements. It examines what the system does without concern for how it does it.

Unit Testing

Unit testing verifies that individual components or functions work correctly in isolation.

Unit Testing a Form Validator Function


// Function to test - validates a form field
function validateField(value, rules) {
  if (rules.required && (!value || value.trim() === '')) {
    return { valid: false, error: 'This field is required' };
  }
  
  if (rules.minLength && value.length < rules.minLength) {
    return { 
      valid: false, 
      error: `This field must be at least ${rules.minLength} characters` 
    };
  }
  
  if (rules.maxLength && value.length > rules.maxLength) {
    return { 
      valid: false, 
      error: `This field cannot exceed ${rules.maxLength} characters` 
    };
  }
  
  if (rules.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
    return { valid: false, error: 'Please enter a valid email address' };
  }
  
  return { valid: true, error: null };
}

// Unit tests for the validator
describe('validateField function', () => {
  test('should validate required fields', () => {
    expect(validateField('', { required: true })).toEqual({
      valid: false,
      error: 'This field is required'
    });
    
    expect(validateField('Hello', { required: true })).toEqual({
      valid: true,
      error: null
    });
  });
  
  test('should validate minimum length', () => {
    expect(validateField('abc', { minLength: 5 })).toEqual({
      valid: false,
      error: 'This field must be at least 5 characters'
    });
    
    expect(validateField('abcdef', { minLength: 5 })).toEqual({
      valid: true,
      error: null
    });
  });
  
  test('should validate maximum length', () => {
    expect(validateField('abcdefghijk', { maxLength: 5 })).toEqual({
      valid: false,
      error: 'This field cannot exceed 5 characters'
    });
    
    expect(validateField('abc', { maxLength: 5 })).toEqual({
      valid: true,
      error: null
    });
  });
  
  test('should validate email format', () => {
    expect(validateField('notanemail', { email: true })).toEqual({
      valid: false,
      error: 'Please enter a valid email address'
    });
    
    expect(validateField('valid@example.com', { email: true })).toEqual({
      valid: true,
      error: null
    });
  });
  
  test('should validate against multiple rules', () => {
    const rules = {
      required: true,
      minLength: 5,
      maxLength: 50,
      email: true
    };
    
    expect(validateField('', rules)).toEqual({
      valid: false,
      error: 'This field is required'
    });
    
    expect(validateField('a@b.c', rules)).toEqual({
      valid: false,
      error: 'This field must be at least 5 characters'
    });
    
    expect(validateField('verylongemailaddress@reallylongdomainname.com', rules)).toEqual({
      valid: true,
      error: null
    });
    
    expect(validateField('notanemail', rules)).toEqual({
      valid: false,
      error: 'Please enter a valid email address'
    });
  });
});
        

Analogy: Unit Testing as Quality Control at a Factory

Unit testing is like quality control for individual components on a factory assembly line. Before assembling a car, each part (engine, transmission, brakes) is tested individually to ensure it meets specifications. This allows defects to be caught early, before the components are assembled into the final product.

Similarly, unit tests verify that each "part" of your code works correctly in isolation, making it easier to identify and fix issues before they affect the entire application.

Unit Testing Best Practices

Integration Testing

Integration testing verifies that multiple units work together correctly. It tests the interactions between integrated components or systems.

Integration Testing User Registration


// Integration test for user registration process
describe('User Registration Integration', () => {
  let userService;
  let emailService;
  let dbClient;
  
  beforeEach(() => {
    // Set up real database connection for integration test
    dbClient = new DatabaseClient(testDbConfig);
    
    // Create real UserService but mock EmailService
    emailService = {
      sendWelcomeEmail: jest.fn().mockResolvedValue(true)
    };
    
    userService = new UserService(dbClient, emailService);
  });
  
  afterEach(async () => {
    // Clean up test data
    await dbClient.collection('users').deleteMany({ email: 'test@example.com' });
    await dbClient.disconnect();
  });
  
  test('should register new user and send welcome email', async () => {
    // Test complete registration flow
    const userData = {
      name: 'Test User',
      email: 'test@example.com',
      password: 'securePassword123'
    };
    
    const result = await userService.registerUser(userData);
    
    // Verify user was created in database
    const createdUser = await dbClient.collection('users')
      .findOne({ email: userData.email });
    
    expect(createdUser).not.toBeNull();
    expect(createdUser.name).toBe(userData.name);
    expect(createdUser.email).toBe(userData.email);
    
    // Password should be hashed, not stored as plaintext
    expect(createdUser.password).not.toBe(userData.password);
    
    // Verify welcome email was sent
    expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(
      expect.objectContaining({
        name: userData.name,
        email: userData.email
      })
    );
    
    // Verify returned result
    expect(result.success).toBe(true);
    expect(result.userId).toBe(createdUser._id.toString());
  });
  
  test('should handle duplicate email registration', async () => {
    // First registration
    await userService.registerUser({
      name: 'Test User',
      email: 'test@example.com',
      password: 'password123'
    });
    
    // Attempt duplicate registration
    const result = await userService.registerUser({
      name: 'Another User',
      email: 'test@example.com',
      password: 'different123'
    });
    
    // Verify error handling
    expect(result.success).toBe(false);
    expect(result.error).toBe('Email already registered');
    
    // Verify no duplicate user created
    const users = await dbClient.collection('users')
      .find({ email: 'test@example.com' })
      .toArray();
    
    expect(users.length).toBe(1);
    
    // Verify no welcome email sent for failed registration
    expect(emailService.sendWelcomeEmail).toHaveBeenCalledTimes(1);
  });
});
        
graph TD A[Client Request] --> B[API Layer] B --> C[UserService] C --> D[DatabaseClient] C --> E[EmailService] D --> F[(Database)] E --> G[Email Provider] style A fill:#f9f9f9,stroke:#333 style B,C fill:#d5e8d4,stroke:#82b366 style D,E fill:#dae8fc,stroke:#6c8ebf style F,G fill:#f5f5f5,stroke:#666 I[Integration Test] -.-> B I -.-> C I -.-> D I -.-> F

Integration Testing Strategies

Integration Testing Challenges and Solutions

Challenge Solution
Complex setup Use test containers or local environments
Slow execution Test only critical integration points
External dependencies Use stubs or mock services
Data consistency Reset data between test runs
Flaky tests Implement retry mechanisms and timeouts

System Testing

System testing evaluates the complete, integrated software system to ensure it meets specified requirements.

Analogy: System Testing as Test Driving a Car

If unit testing is checking individual car components and integration testing is making sure those components work together, system testing is like taking the fully assembled car for a test drive on various road conditions.

During the test drive, you're checking that the entire vehicle performs as expected in real-world scenarios, just as system testing evaluates the complete application in conditions similar to how end users will experience it.

Acceptance Testing

Acceptance testing determines if the software meets business requirements and is ready for delivery. Types include:

Example: Behavior-Driven Development (BDD) for Acceptance Testing


# Feature file written in Gherkin syntax (feature.feature)
Feature: User Registration
  As a website visitor
  I want to register for an account
  So that I can access member-only content

  Scenario: Successful registration
    Given I am on the registration page
    When I enter valid registration details
    And I submit the registration form
    Then I should see a welcome message
    And I should receive a confirmation email

  Scenario: Registration with existing email
    Given I am on the registration page
    When I enter an email that is already registered
    And I submit the registration form
    Then I should see an error message
    And I should remain on the registration page
    And my password field should be cleared
        

// Implementation of BDD steps in JavaScript with Cucumber
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('chai');
const { Builder, By } = require('selenium-webdriver');

let driver;
let email;

Given('I am on the registration page', async function() {
  driver = await new Builder().forBrowser('chrome').build();
  await driver.get('https://example.com/register');
  
  // Verify we're on the registration page
  const title = await driver.getTitle();
  expect(title).to.include('Register');
});

When('I enter valid registration details', async function() {
  // Generate a unique email
  email = `test.${Date.now()}@example.com`;
  
  await driver.findElement(By.id('name')).sendKeys('Test User');
  await driver.findElement(By.id('email')).sendKeys(email);
  await driver.findElement(By.id('password')).sendKeys('SecurePassword123!');
  await driver.findElement(By.id('confirm-password')).sendKeys('SecurePassword123!');
});

When('I enter an email that is already registered', async function() {
  // Use a known registered email
  email = 'existing@example.com';
  
  await driver.findElement(By.id('name')).sendKeys('Test User');
  await driver.findElement(By.id('email')).sendKeys(email);
  await driver.findElement(By.id('password')).sendKeys('SecurePassword123!');
  await driver.findElement(By.id('confirm-password')).sendKeys('SecurePassword123!');
});

When('I submit the registration form', async function() {
  await driver.findElement(By.css('button[type="submit"]')).click();
});

Then('I should see a welcome message', async function() {
  // Wait for the welcome message to appear
  const welcomeMessage = await driver.wait(
    until.elementLocated(By.className('welcome-message')),
    5000
  );
  
  const text = await welcomeMessage.getText();
  expect(text).to.include('Welcome');
  expect(text).to.include('Test User');
});

Then('I should see an error message', async function() {
  // Wait for the error message to appear
  const errorMessage = await driver.wait(
    until.elementLocated(By.className('error-message')),
    5000
  );
  
  const text = await errorMessage.getText();
  expect(text).to.include('already registered');
});

// Additional step implementations...

After(async function() {
  // Clean up
  if (driver) {
    await driver.quit();
  }
});
        

Non-Functional Testing

Non-functional testing evaluates aspects of the software not related to specific behaviors or functions, such as performance, usability, and security.

Performance Testing

Performance testing evaluates how a system performs under various conditions, focusing on responsiveness and stability.

graph TD A[Performance Testing] --> B[Load Testing] A --> C[Stress Testing] A --> D[Endurance Testing] A --> E[Spike Testing] A --> F[Scalability Testing] B --> B1[Simulate expected load] C --> C1[Test beyond normal capacity] D --> D1[Test system over extended periods] E --> E1[Test sudden increase in load] F --> F1[Test how system scales with increased resources]

Example: Load Testing with k6


// load-test.js
import http from 'k6/http';
import { sleep, check } from 'k6';
import { Counter } from 'k6/metrics';

// Custom metric to track errors
const errors = new Counter('errors');

export const options = {
  // Test configuration
  stages: [
    { duration: '1m', target: 50 }, // Ramp up to 50 users over 1 minute
    { duration: '3m', target: 50 }, // Stay at 50 users for 3 minutes
    { duration: '1m', target: 100 }, // Ramp up to 100 users over 1 minute
    { duration: '5m', target: 100 }, // Stay at 100 users for 5 minutes
    { duration: '1m', target: 0 }, // Ramp down to 0 users
  ],
  thresholds: {
    'http_req_duration': ['p(95)<500'], // 95% of requests must complete below 500ms
    'http_req_failed': ['rate<0.01'], // Less than 1% of requests can fail
    'errors': ['count<10'], // Less than 10 errors total
  },
};

export default function() {
  // Load testing scenario for an e-commerce site
  
  // Step 1: Visit the homepage
  let response = http.get('https://example.com/');
  check(response, {
    'homepage status code is 200': (r) => r.status === 200,
    'homepage loaded fast': (r) => r.timings.duration < 300,
  }) || errors.add(1);
  sleep(3);
  
  // Step 2: Search for a product
  response = http.get('https://example.com/products?search=laptop');
  check(response, {
    'search status code is 200': (r) => r.status === 200,
    'search contains results': (r) => r.body.includes('Product Results'),
    'search loaded under 500ms': (r) => r.timings.duration < 500,
  }) || errors.add(1);
  sleep(2);
  
  // Step 3: View a product
  response = http.get('https://example.com/products/laptop-pro-2025');
  check(response, {
    'product page status code is 200': (r) => r.status === 200,
    'product page contains add to cart button': (r) => r.body.includes('Add to Cart'),
  }) || errors.add(1);
  sleep(3);
  
  // Step 4: Add to cart
  response = http.post('https://example.com/cart/add', {
    productId: 'laptop-pro-2025',
    quantity: 1
  });
  check(response, {
    'add to cart successful': (r) => r.status === 200,
    'cart updated message appears': (r) => r.body.includes('added to your cart'),
  }) || errors.add(1);
  sleep(1);
  
  // Step 5: View cart
  response = http.get('https://example.com/cart');
  check(response, {
    'cart page status code is 200': (r) => r.status === 200,
    'cart contains added product': (r) => r.body.includes('Laptop Pro 2025'),
  }) || errors.add(1);
  sleep(3);
}
        

Performance Testing Metrics

Security Testing

Security testing identifies vulnerabilities in the software that could be exploited by malicious users.

graph TD A[Security Testing] --> B[Vulnerability Scanning] A --> C[Penetration Testing] A --> D[Security Auditing] A --> E[Risk Assessment] A --> F[Security Code Review] A --> G[Compliance Testing]

Common Security Testing Areas

  • Authentication: Testing login mechanisms, password policies, session management
  • Authorization: Verifying access control, role-based permissions
  • Data Protection: Testing encryption, secure storage, data leakage prevention
  • Input Validation: Testing for injection attacks (SQL, XSS, CSRF)
  • API Security: Testing API endpoints for vulnerabilities
  • Dependency Security: Scanning for vulnerable third-party libraries

Usability Testing

Usability testing evaluates how user-friendly and intuitive the software is from an end user's perspective.

Analogy: Usability Testing as Restaurant Reviews

Usability testing is like having food critics visit your restaurant. They're not checking if the kitchen follows health codes (functional testing) or if the building can withstand an earthquake (security testing). Instead, they're evaluating the dining experience: Is the menu easy to understand? Is the service prompt? Is the atmosphere pleasant?

Just as a restaurant might make changes based on critic feedback to improve the dining experience, developers make changes based on usability testing to improve the user experience.

Accessibility Testing

Accessibility testing ensures that the application can be used by people with disabilities, including visual, auditory, physical, speech, cognitive, and neurological disabilities.

Accessibility Testing Checklist

  • All images have meaningful alt text
  • Color contrast meets WCAG standards
  • Forms have proper labels and instructions
  • Keyboard navigation works for all interactions
  • Screen readers can access all content
  • No content flashes at rates that could trigger seizures
  • Error messages are clear and helpful
  • Page structure uses proper heading hierarchy
  • Interactive elements are clearly identifiable
  • Time-dependent functions can be adjusted or paused

Compatibility Testing

Compatibility testing verifies that the software works correctly across different environments, devices, operating systems, browsers, and network conditions.

Testing by Stage

Some testing types are categorized by when they occur in the development lifecycle.

Alpha Testing

Alpha testing is performed in a controlled environment, typically by internal testers or QA teams.

Beta Testing

Beta testing involves distributing the software to a limited number of external users to test it in real-world environments.

Real-World Beta Testing Examples

  • Gmail: Remained in beta for 5 years (2004-2009) while Google refined features based on user feedback
  • iOS Public Beta Program: Apple releases pre-release versions to millions of users to identify bugs before official releases
  • Microsoft Flight Simulator 2020: Used a closed beta to test the game's performance across different hardware configurations
  • Slack: Started as a beta for their own internal team before expanding to other companies

Regression Testing

Regression testing ensures that new changes haven't adversely affected existing functionality.

Analogy: Regression Testing as Home Maintenance

Regression testing is like regularly checking your home after making any modifications. If you remodel your kitchen, you need to verify that the electrical, plumbing, and HVAC systems still work properly throughout the house, not just in the remodeled area.

Just as neglecting these checks could lead to unpleasant surprises (like discovering your upstairs shower no longer works after remodeling the kitchen), skipping regression testing can lead to unexpected breaks in functionality that was working fine before recent changes.

Testing by Approach

Testing approaches define how testers interact with the software and what information they have access to.

Static vs. Dynamic Testing

Static Testing Dynamic Testing
Done without executing code Requires executing the code
Reviews, inspections, walkthroughs Unit, integration, system testing
Catches issues early in development Finds runtime issues and behavior problems
Lower cost to fix identified issues More thorough verification of behavior

Code Review as Static Testing

Code reviews are a form of static testing where developers examine each other's code for issues.

Code Review Checklist

  • Correctness: Does the code correctly implement the requirements?
  • Architecture: Does the code fit the overall system architecture?
  • Maintainability: Is the code clear, well-documented, and easy to maintain?
  • Performance: Are there any performance concerns with the implementation?
  • Security: Does the code introduce any security vulnerabilities?
  • Error Handling: Are exceptions and edge cases properly handled?
  • Testing: Are there appropriate tests for the code?
  • Standards: Does the code follow project style guidelines?

White Box Testing

White box testing (also called clear box or glass box testing) examines the internal structure of the code.

Code Coverage Metrics in White Box Testing

Statement vs. Branch Coverage Example


function processPayment(amount, accountBalance) {
  // Statement 1
  if (amount <= 0) {
    // Statement 2 (Branch 1)
    return { success: false, message: 'Invalid amount' };
  }
  
  // Statement 3
  if (amount > accountBalance) {
    // Statement 4 (Branch 2)
    return { success: false, message: 'Insufficient funds' };
  }
  
  // Statement 5
  const newBalance = accountBalance - amount;
  
  // Statement 6
  return { success: true, newBalance: newBalance };
}

// Test achieving 100% statement coverage but only 50% branch coverage
test('processPayment with valid amount', () => {
  const result = processPayment(50, 100);
  expect(result.success).toBe(true);
  expect(result.newBalance).toBe(50);
});

// Additional test needed for 100% branch coverage
test('processPayment with invalid amount', () => {
  const result = processPayment(-10, 100);
  expect(result.success).toBe(false);
  expect(result.message).toBe('Invalid amount');
});

// One more test for full branch coverage
test('processPayment with insufficient funds', () => {
  const result = processPayment(150, 100);
  expect(result.success).toBe(false);
  expect(result.message).toBe('Insufficient funds');
});
        

Black Box Testing

Black box testing examines the functionality of an application without knowledge of its internal structure.

graph LR A[Inputs] --> B[Black Box System] B --> C[Expected Outputs] D[Test Cases] -.-> A D -.-> C

Black Box Testing Techniques

  • Equivalence Partitioning: Divide inputs into valid and invalid classes and test one value from each
  • Boundary Value Analysis: Test values at the boundaries of input ranges
  • Decision Table Testing: Test different combinations of inputs
  • State Transition Testing: Test the system as it moves through different states
  • Use Case Testing: Test based on user scenarios

Gray Box Testing

Gray box testing combines elements of both white box and black box testing, with limited knowledge of the internal workings.

Analogy: Testing Approaches as Home Inspection

The different testing approaches can be compared to different types of home inspection:

  • White Box Testing: Like a construction inspector who has the blueprints, knows what's behind the walls, and checks if everything was built according to code and specifications.
  • Black Box Testing: Like a potential homebuyer who evaluates the house based only on what they can see and experience - do the lights work, does the water run, are the rooms comfortable?
  • Gray Box Testing: Like a home inspector who knows general construction principles and has some tools to peek into certain areas (like moisture meters or thermal cameras) but doesn't have full blueprints or the ability to open up walls.

Testing Methodologies

Testing methodologies are systematic approaches to testing that encompass multiple testing types and techniques.

Test-Driven Development (TDD)

TDD is a development process where tests are written before the code that needs to be tested.

graph TD A[Write a Failing Test] --> B[Write Minimal Code to Pass] B --> C[Refactor Code] C --> A

TDD Workflow Example

Step 1: Write a failing test


// Test for a function that doesn't exist yet
test('calculateDiscountedPrice applies correct discount', () => {
  expect(calculateDiscountedPrice(100, 20)).toBe(80);
  expect(calculateDiscountedPrice(50, 10)).toBe(45);
  expect(calculateDiscountedPrice(200, 0)).toBe(200);
});

// Running this test would fail since the function doesn't exist
        

Step 2: Write minimal code to pass the test


// Implement the minimal code to make the test pass
function calculateDiscountedPrice(price, discountPercentage) {
  return price - (price * discountPercentage / 100);
}

// Now the test should pass
        

Step 3: Refactor while keeping tests passing


// Refactor to handle edge cases and improve code
function calculateDiscountedPrice(price, discountPercentage) {
  // Validate inputs
  if (typeof price !== 'number' || price < 0) {
    throw new Error('Price must be a positive number');
  }
  
  if (typeof discountPercentage !== 'number' || discountPercentage < 0 || discountPercentage > 100) {
    throw new Error('Discount must be a number between 0 and 100');
  }
  
  // Calculate discounted price
  return price - (price * discountPercentage / 100);
}

// Add new tests for the edge cases
test('calculateDiscountedPrice validates inputs', () => {
  expect(() => calculateDiscountedPrice(-100, 20)).toThrow();
  expect(() => calculateDiscountedPrice('100', 20)).toThrow();
  expect(() => calculateDiscountedPrice(100, 120)).toThrow();
  expect(() => calculateDiscountedPrice(100, -10)).toThrow();
});
        

Behavior-Driven Development (BDD)

BDD extends TDD by focusing on the behavior of the application from the user's perspective, often using natural language specifications.

BDD vs. Traditional Requirements

Traditional Requirement:

"The system shall allow users to reset their password."

BDD Scenario:


Feature: Password Reset
  As a registered user
  I want to reset my password
  So that I can regain access to my account if I forget my password

  Scenario: User resets password successfully
    Given I am on the login page
    When I click on the "Forgot Password" link
    And I enter my registered email address
    And I click on the "Reset Password" button
    Then I should see a message confirming that reset instructions have been sent
    And I should receive an email with password reset instructions
    
  Scenario: User enters unregistered email for password reset
    Given I am on the password reset page
    When I enter an email address that is not registered
    And I click on the "Reset Password" button
    Then I should see an error message indicating the email is not registered
        

Continuous Testing

Continuous Testing is the process of executing automated tests as part of the software delivery pipeline to obtain immediate feedback on business risks.

graph LR A[Code] --> B[Build] B --> C[Unit Tests] C --> D[Integration Tests] D --> E[System Tests] E --> F[Deploy to Staging] F --> G[Acceptance Tests] G --> H[Deploy to Production] C -.->|Fail| A D -.->|Fail| A E -.->|Fail| A G -.->|Fail| A

CI/CD Pipeline with Testing Stages


# Example GitHub Actions workflow with multiple testing stages
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  # Stage 1: Linting and static analysis
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Lint code
        run: npm run lint
      - name: Static code analysis
        run: npm run analyze

  # Stage 2: Unit testing
  unit-test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Run unit tests
        run: npm test
      - name: Upload coverage
        uses: codecov/codecov-action@v2

  # Stage 3: Integration testing
  integration-test:
    needs: unit-test
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: test_db
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Run integration tests
        run: npm run test:integration
        env:
          DATABASE_URL: postgres://postgres:postgres@localhost:5432/test_db

  # Stage 4: Build and E2E tests
  build-and-e2e:
    needs: integration-test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Build application
        run: npm run build
      - name: Start application
        run: npm start & npx wait-on http://localhost:3000
      - name: Run E2E tests
        run: npm run test:e2e
      - name: Archive build artifacts
        uses: actions/upload-artifact@v2
        with:
          name: build
          path: build/

  # Stage 5: Security testing
  security-scan:
    needs: build-and-e2e
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Check for vulnerabilities
        run: npm audit --audit-level=high
      - name: OWASP ZAP Scan
        uses: zaproxy/action-baseline@v0.6.1
        with:
          target: 'http://localhost:3000'

  # Stage 6: Deploy to staging
  deploy-staging:
    needs: [build-and-e2e, security-scan]
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v2
        with:
          name: build
          path: build/
      - name: Deploy to staging
        run: |
          # Deploy commands here
          echo "Deploying to staging environment"

  # Stage 7: Acceptance tests on staging
  acceptance-tests:
    needs: deploy-staging
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Run acceptance tests against staging
        run: npm run test:acceptance
        env:
          TEST_URL: https://staging.example.com

  # Stage 8: Deploy to production
  deploy-production:
    needs: acceptance-tests
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v2
        with:
          name: build
          path: build/
      - name: Deploy to production
        run: |
          # Production deployment commands
          echo "Deploying to production environment"
        

Practical Exercise

Testing Strategy Design

For this exercise, you'll develop a comprehensive testing strategy for a web application. Choose one of the following application types:

  1. E-commerce website
  2. Social media platform
  3. Online banking application
  4. Content management system
  5. Healthcare portal

For your chosen application, create a testing strategy document that includes:

  1. Testing objectives and scope
  2. Key features to test
  3. Types of testing to be performed
  4. Testing tools and environments
  5. Test data requirements
  6. Risk assessment and mitigation
  7. Testing timeline and resources
  8. Reporting and metrics

Sample Testing Strategy Outline for an E-commerce Website

Testing Objectives and Scope

  • Ensure all critical user journeys function correctly
  • Verify the system can handle expected user load
  • Confirm security of user data and payment processing
  • Validate compatibility across major browsers and devices

Key Features to Test

  • User authentication and account management
  • Product browsing and search functionality
  • Shopping cart and checkout process
  • Payment processing and order confirmation
  • User reviews and ratings
  • Admin dashboard and inventory management

Types of Testing

Testing Type Scope Priority
Unit Testing Core business logic, utilities, helpers High
Integration Testing API endpoints, database interactions High
End-to-End Testing Critical user flows (registration, checkout) High
Performance Testing Search, product listing, checkout Medium
Security Testing Authentication, payment, data protection High
Compatibility Testing Top 5 browsers, mobile/tablet/desktop Medium
Accessibility Testing All public-facing pages Medium

Testing Tools and Environments

  • Unit Testing: Jest, React Testing Library
  • E2E Testing: Cypress
  • API Testing: Postman, Supertest
  • Performance Testing: k6
  • Security Testing: OWASP ZAP, npm audit
  • Accessibility Testing: axe, Lighthouse
  • Environments: Development, Testing, Staging, Production

Test Data Requirements

  • Sample user profiles with different roles and permissions
  • Product catalog with various categories, prices, and attributes
  • Test credit cards for payment testing
  • Order history data for reporting and analytics testing

Risk Assessment and Mitigation

Risk Impact Likelihood Mitigation
Payment processing failure High Medium Thorough testing of payment flows, fallback mechanisms
Data breach High Low Security testing, code reviews, encryption
Performance issues during sales Medium High Load testing, performance monitoring, scaling plan
Browser compatibility issues Medium Medium Cross-browser testing, progressive enhancement

Summary

In this lecture, we've explored the rich landscape of testing types and methodologies, including:

Understanding these different testing types and when to apply them is crucial for building a comprehensive testing strategy that ensures your applications are robust, reliable, and meet user expectations.

In the next lecture, we'll dive into Test-Driven Development (TDD) workflow and explore how to implement it effectively in your projects.

Additional Resources

Books

Online Resources

Tools