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.
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
- Keep Tests Fast: Unit tests should execute quickly to enable frequent runs
- Test One Thing per Test: Each test should verify a single behavior
- Use Descriptive Names: Test names should describe what is being tested
- Arrange-Act-Assert: Structure tests in three clear phases
- Focus on Behavior: Test what the code does, not how it's implemented
- Use Mocks Judiciously: Mock external dependencies but don't over-mock
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);
});
});
Integration Testing Strategies
- Big Bang: Integrate all components at once and test the complete system
- Top-Down: Start with high-level components and gradually integrate lower-level ones
- Bottom-Up: Start with low-level components and gradually integrate higher-level ones
- Sandwich/Hybrid: Combine top-down and bottom-up approaches
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.
- Tests the entire application as a whole
- Validates both functional and non-functional requirements
- Often performed in an environment similar to production
- Typically conducted by dedicated testing teams
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:
- User Acceptance Testing (UAT): End users test the software to determine if it meets their needs
- Business Acceptance Testing (BAT): Validates the software against business requirements
- Operational Acceptance Testing (OAT): Verifies the system is ready for production (backups, recovery, security)
- Contract Acceptance Testing: Verifies the software meets contractual requirements
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.
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
- Response Time: Time taken to respond to a request
- Throughput: Number of requests processed per time unit
- Error Rate: Percentage of requests resulting in errors
- Concurrent Users: Number of active users at a time
- Resource Utilization: CPU, memory, network, disk usage
- Bottlenecks: Components limiting overall performance
Security Testing
Security testing identifies vulnerabilities in the software that could be exploited by malicious users.
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.
- Often involves real users performing specific tasks
- Focuses on ease of use, user satisfaction, and learnability
- Can include metrics like task completion time and error rates
- May involve techniques like eye tracking and heatmaps
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.
- Browser Compatibility: Testing across Chrome, Firefox, Safari, Edge, etc.
- Device Compatibility: Testing on desktops, tablets, mobile phones
- OS Compatibility: Testing on Windows, macOS, Linux, iOS, Android
- Network Compatibility: Testing under various 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.
- Conducted before the software is released to external users
- Focuses on identifying major defects
- Often involves simulated or actual operational conditions
- Developers may be directly involved in fixing issues
Beta Testing
Beta testing involves distributing the software to a limited number of external users to test it in real-world environments.
- Users report bugs and provide feedback on their experience
- Tests the software in diverse, real-world conditions
- Helps identify issues that internal testing might miss
- Can also gauge market acceptance and gather feature requests
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.
- Performed after code changes, bug fixes, or new feature additions
- Often automated to run consistently and frequently
- Focuses on retesting previously functioning areas
- Critical for maintaining software stability over time
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.
- Testers have full knowledge of the internal design and code
- Tests are designed based on the code's structure
- Aims for comprehensive code coverage
- Often performed by developers or technical testers
Code Coverage Metrics in White Box Testing
- Statement Coverage: Percentage of code statements executed
- Branch Coverage: Percentage of decision branches executed
- Path Coverage: Percentage of possible paths through the code executed
- Function Coverage: Percentage of functions called
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.
- Testers have no knowledge of the internal code or design
- Tests are designed based on specifications and requirements
- Focuses on inputs and outputs
- Often performed by dedicated testers or end users
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.
- Testers have partial knowledge of the internal structure
- Combines design-based and specification-based testing
- More informed than black box but less intrusive than white box
- Often used for integration testing and penetration testing
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.
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.
- Uses a ubiquitous language understood by all stakeholders
- Often employs Given-When-Then format for scenarios
- Bridges communication between technical and non-technical team members
- Popular tools include Cucumber, SpecFlow, and Behat
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.
- Tests are run at every stage of the CI/CD pipeline
- Provides rapid feedback on code changes
- Reduces the risk of deploying defects to production
- Requires significant automation and infrastructure
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:
- E-commerce website
- Social media platform
- Online banking application
- Content management system
- Healthcare portal
For your chosen application, create a testing strategy document that includes:
- Testing objectives and scope
- Key features to test
- Types of testing to be performed
- Testing tools and environments
- Test data requirements
- Risk assessment and mitigation
- Testing timeline and resources
- 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:
- Functional Testing: Unit, integration, system, and acceptance testing
- Non-Functional Testing: Performance, security, usability, accessibility, and compatibility testing
- Testing by Stage: Alpha, beta, and regression testing
- Testing by Approach: White box, black box, and gray box testing
- Testing Methodologies: TDD, BDD, and continuous testing
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
- "Effective Software Testing: A Developer's Guide" by Mauricio Aniche
- "How Google Tests Software" by James A. Whittaker, Jason Arbon, and Jeff Carollo
- "The Art of Software Testing" by Glenford J. Myers
Online Resources
- Guru99 Software Testing Tutorial
- Atlassian Testing in CI/CD
- Web.dev Accessibility Testing
- k6 Performance Testing Documentation