ποΈ Testing Types and Methodologies
"Testing" is not one thing β it's a whole toolbox. This lesson maps the landscape: functional tests that check what the software does, non-functional tests that check how well it does it, and the different stages and approaches you'll draw on to build a complete strategy.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish functional from non-functional testing and give examples of each
- Explain the roles of unit, integration, system, and acceptance testing
- Describe key non-functional types β performance, security, usability, accessibility, compatibility
- Compare testing by stage (alpha, beta, regression) and by approach (white/black/gray box)
- Assemble these types into a coherent testing strategy for a real project
Estimated Time: 40β50 minutes β’ Difficulty: Intermediate
Hands-on: Build a testing-strategy table matching test types to features of an app you choose.
In This Lesson
The Testing Landscape
In the previous lesson you learned the principles of testing. Now we'll survey the types. It helps to organize them along a few independent axes: what a test verifies (functional vs non-functional), when it runs (by stage), and how much the tester knows about the internals (by approach). A real strategy mixes all three.
π Two big buckets
Functional testing asks "does it do the right thing?" β it validates behavior against requirements.
Non-functional testing asks "does it do it well?" β speed, security, accessibility, and other qualities.
Functional Testing
Functional testing validates that the software behaves per its specification. It's usually organized by scope, from the smallest unit up to the whole system.
Unit testing
Verifies a single function or class in isolation. Here's a form-field validator tested thoroughly with modern Vitest (the syntax is identical in Jest):
// validateField.js
export 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: `Must be at least ${rules.minLength} characters` };
}
if (rules.maxLength && value.length > rules.maxLength) {
return { valid: false, error: `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 };
}
// validateField.test.js
import { describe, it, expect } from 'vitest';
import { validateField } from './validateField.js';
describe('validateField', () => {
it('flags an empty required field', () => {
expect(validateField('', { required: true })).toEqual({
valid: false, error: 'This field is required',
});
});
it('enforces a minimum length', () => {
expect(validateField('abc', { minLength: 5 }).valid).toBe(false);
expect(validateField('abcdef', { minLength: 5 }).valid).toBe(true);
});
it('validates email format', () => {
expect(validateField('nope', { email: true }).valid).toBe(false);
expect(validateField('a@b.com', { email: true }).valid).toBe(true);
});
it('reports the first failing rule when several apply', () => {
const rules = { required: true, minLength: 5, email: true };
expect(validateField('', rules).error).toBe('This field is required');
expect(validateField('a@b', rules).error).toContain('at least 5');
});
});
π‘ Factory quality control: unit testing is checking each car part β engine, brakes, transmission β before assembly. Catch a faulty part on the bench and you never have to diagnose it inside a finished vehicle.
β Unit testing best practices
- Fast: milliseconds each, so you can run them constantly.
- One behavior per test with a descriptive name.
- ArrangeβActβAssert: set up, run, then check β in that order.
- Test behavior, not internal implementation.
- Mock external dependencies judiciously β over-mocking makes tests meaningless.
Integration testing
Verifies that units cooperate β the seams between modules, where bugs love to hide. Here a real database is used but the email provider is mocked:
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { UserService } from '../src/UserService.js';
import { DatabaseClient } from '../src/DatabaseClient.js';
describe('user registration integration', () => {
let userService, emailService, db;
beforeEach(() => {
db = new DatabaseClient(testDbConfig);
emailService = { sendWelcomeEmail: vi.fn().mockResolvedValue(true) };
userService = new UserService(db, emailService);
});
afterEach(async () => {
await db.collection('users').deleteMany({ email: 'test@example.com' });
await db.disconnect();
});
it('creates the user and sends a welcome email', async () => {
const data = { name: 'Test User', email: 'test@example.com', password: 'secret123' };
const result = await userService.registerUser(data);
const saved = await db.collection('users').findOne({ email: data.email });
expect(saved).not.toBeNull();
expect(saved.password).not.toBe(data.password); // must be hashed
expect(emailService.sendWelcomeEmail).toHaveBeenCalledWith(
expect.objectContaining({ email: data.email })
);
expect(result.success).toBe(true);
});
it('rejects a duplicate email', async () => {
await userService.registerUser({ name: 'A', email: 'test@example.com', password: 'p1' });
const result = await userService.registerUser({ name: 'B', email: 'test@example.com', password: 'p2' });
expect(result.success).toBe(false);
expect(result.error).toBe('Email already registered');
expect(emailService.sendWelcomeEmail).toHaveBeenCalledTimes(1);
});
});
π‘ Integration strategies & challenges
Components can be integrated top-down, bottom-up, or all at once ("big bang"). Common headaches and fixes:
| Challenge | Solution |
|---|---|
| Complex setup | Use test containers (e.g. Testcontainers) or a disposable local DB |
| Slow execution | Test only critical integration points |
| External services | Stub or mock them at the boundary |
| Data consistency | Reset/seed data between runs |
| Flaky tests | Fix root causes; use explicit waits, not sleeps |
System testing
Evaluates the complete, integrated application against its requirements β usually in a production-like environment. If unit tests check the parts and integration tests check the connections, system testing is the full test drive of the assembled car on real roads.
Acceptance testing
Determines whether the software meets business needs and is ready to deliver. Variants include User Acceptance Testing (UAT), Business Acceptance Testing, and Operational Acceptance Testing (backups, recovery, monitoring). Acceptance tests are often expressed in plain-language Gherkin so non-developers can read them:
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
Those steps are then automated. A modern, browser-based implementation with Playwright is far cleaner than the older Selenium style:
import { test, expect } from '@playwright/test';
test('a visitor can register successfully', async ({ page }) => {
await page.goto('/register');
const email = `test.${Date.now()}@example.com`;
await page.getByLabel('Name').fill('Test User');
await page.getByLabel('Email').fill(email);
await page.getByLabel('Password', { exact: true }).fill('SecurePass123!');
await page.getByLabel('Confirm password').fill('SecurePass123!');
await page.getByRole('button', { name: 'Register' }).click();
await expect(page.getByText(/welcome, test user/i)).toBeVisible();
});
Non-Functional Testing
Non-functional testing measures qualities beyond raw behavior β the difference between a feature that works and one that works well.
Performance testing
Measures responsiveness and stability under load. It has several flavors β load, stress, endurance (soak), spike, and scalability testing.
A load test with k6 ramps virtual users up and asserts on latency and error thresholds:
// load-test.js
import http from 'k6/http';
import { sleep, check } from 'k6';
export const options = {
stages: [
{ duration: '1m', target: 50 }, // ramp to 50 users
{ duration: '3m', target: 50 }, // hold
{ duration: '1m', target: 100 }, // ramp to 100
{ duration: '5m', target: 100 }, // hold
{ duration: '1m', target: 0 }, // ramp down
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% of requests under 500ms
http_req_failed: ['rate<0.01'], // under 1% failures
},
};
export default function () {
const res = http.get('https://example.com/products?search=laptop');
check(res, {
'status is 200': (r) => r.status === 200,
'responds under 500ms': (r) => r.timings.duration < 500,
});
sleep(2);
}
Key metrics to watch: response time, throughput, error rate, concurrent users, and resource utilization (CPU, memory).
Security testing
Hunts for vulnerabilities an attacker could exploit. It spans vulnerability scanning, penetration testing, dependency auditing, and secure code review.
β οΈ Common security testing areas
- Authentication: login flows, password policies, session handling
- Authorization: access control and role-based permissions
- Input validation: injection attacks β SQL, XSS, CSRF
- Data protection: encryption in transit and at rest
- Dependencies: scanning third-party packages (
npm audit, Dependabot)
Usability testing
Evaluates how intuitive the product feels, usually by watching real users attempt real tasks and measuring completion rates and errors.
π‘ Restaurant critics: usability testing isn't checking the kitchen's hygiene (functional) or the building's structure (security). It's asking whether the meal was a pleasure β clear menu, prompt service, good experience.
Accessibility testing
Ensures people with disabilities can use the app, guided by the WCAG standard. A quick checklist:
- Meaningful
alttext on informative images - Color contrast meets WCAG AA
- Every interactive element is keyboard-reachable
- Form fields have associated labels
- Proper heading hierarchy and landmark structure
- No content flashes at seizure-inducing rates
Automate the low-hanging fruit with tools like axe-core or Lighthouse β they catch a large share of common issues in seconds.
Compatibility testing
Confirms the app works across browsers (Chrome, Firefox, Safari, Edge), devices (desktop, tablet, phone), and operating systems. Playwright can run the same suite across multiple browser engines out of the box.
Testing by Stage
Some testing is defined by when in the release cycle it happens.
| Stage | Who | Purpose |
|---|---|---|
| Alpha | Internal testers / QA | Catch major defects in a controlled environment before any external release |
| Beta | A limited group of real users | Surface issues in diverse real-world conditions; gauge acceptance |
| Regression | Usually automated | Confirm new changes didn't break previously working features |
π‘ Beta testing in the wild
Gmail famously wore a "beta" label for five years while Google refined it. Apple's public iOS betas put pre-release builds in front of millions to shake out bugs before launch. Beta testing buys real-world coverage that no internal team can match.
π‘ Regression = home maintenance: remodel the kitchen and you still check that the upstairs shower and the fuse box work. Changing one part of an app can quietly break another; regression tests are the walk-through that catches it.
Testing by Approach
Approaches describe how much the tester knows about the internals, and whether code runs at all.
Static vs dynamic
| Static testing | Dynamic testing |
|---|---|
| Done without running the code | Requires executing the code |
| Reviews, linting, type checks, inspections | Unit, integration, system tests |
| Catches issues extremely early | Finds real runtime behavior problems |
A code review is the classic static test β a peer examines the diff for correctness, security, maintainability, error handling, and adequate test coverage before it merges.
White box, black box, gray box
- White box: full knowledge of the code. Tests target internal structure and aim for coverage metrics β statement, branch, path, and function coverage.
- Black box: no knowledge of internals. Tests are driven by specifications using techniques like equivalence partitioning, boundary value analysis, and state transition testing.
- Gray box: partial knowledge β common for integration and penetration testing.
Statement coverage and branch coverage are not the same thing. This example reaches 100% statements with one test but needs three for full branch coverage:
function processPayment(amount, balance) {
if (amount <= 0) return { success: false, message: 'Invalid amount' }; // branch 1
if (amount > balance) return { success: false, message: 'Insufficient funds' }; // branch 2
return { success: true, newBalance: balance - amount };
}
// Full branch coverage needs all three paths:
it('accepts a valid payment', () => {
expect(processPayment(50, 100)).toEqual({ success: true, newBalance: 50 });
});
it('rejects a non-positive amount', () => {
expect(processPayment(-10, 100).message).toBe('Invalid amount');
});
it('rejects an overdraft', () => {
expect(processPayment(150, 100).message).toBe('Insufficient funds');
});
π‘ Home inspection: a white box tester is the builder with the blueprints who knows what's behind every wall; a black box tester is the buyer who only tries the lights and taps; a gray box tester is the inspector with a moisture meter and general know-how but no full blueprints.
Continuous Testing
Continuous testing runs your automated tests at every stage of the delivery pipeline, so problems are caught within minutes of a commit rather than days later.
A trimmed, modern GitHub Actions workflow wiring the first few stages together:
name: CI
on:
push:
branches: [main, develop]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
- run: npm test -- --coverage
- run: npx playwright install --with-deps
- run: npm run test:e2e
β Why it pays off
- Rapid feedback on every change
- Defects are stopped before they reach production
- A green pipeline becomes the shared definition of "ready to ship"
Hands-on Exercise
ποΈ Draft a Testing Strategy
Objective: Match the right test types to the features of a real application.
Instructions:
- Choose one app type: e-commerce, social platform, online banking, CMS, or healthcare portal.
- List its 5β7 most important features.
- For each feature, choose the test types it needs and a priority (High / Medium / Low).
- Name the tools you'd use for each type (e.g. Vitest, Playwright, k6, axe, OWASP ZAP).
- Add one row for a non-functional concern (performance, security, or accessibility).
π‘ Hint
Start from risk. Money and personal data pull a feature toward High priority and toward security + integration + E2E coverage. Read-only, cosmetic features often need only unit tests. Don't forget at least one non-functional line β performance under sale-day load, or accessibility of public pages.
β Example answer (e-commerce)
| Feature | Test types | Priority | Tools |
|---|---|---|---|
| Authentication | Unit, integration, security | High | Vitest, OWASP ZAP |
| Checkout flow | Integration, E2E | High | Vitest, Playwright |
| Product search | Unit, integration, performance | Medium | Vitest, k6 |
| Reviews | Unit | Low | Vitest |
| Public pages (a11y) | Accessibility | Medium | axe-core, Lighthouse |
| Sale-day traffic | Performance / load | High | k6 |
π― Quick Quiz
Question 1: Which of these is a non-functional test?
Question 2: A tester has no access to the source code and writes tests purely from the spec, focusing on inputs and outputs. This is:
Question 3: What is the main purpose of regression testing?
Summary & Quiz
π Key Takeaways
- Functional testing (unit β integration β system β acceptance) checks what the software does.
- Non-functional testing (performance, security, usability, accessibility, compatibility) checks how well it does it.
- Testing by stage = alpha, beta, regression; testing by approach = white / black / gray box, and static vs dynamic.
- Statement coverage β branch coverage β you often need several tests to exercise every path.
- Continuous testing in the pipeline turns a green build into your shared definition of done.
π Further Reading
- Playwright β end-to-end testing docs
- k6 β performance testing docs
- Atlassian β testing in CI/CD
π What's Next?
You've seen the whole map of test types. Next we zoom into one powerful methodology β Test-Driven Development β and learn the Red-Green-Refactor rhythm that lets tests drive your design.
π Well done!
You can now pick the right test for the right job. Let's put tests first.