Testing Technologies

The Test Pyramid

  • The Test Pyramid is a testing strategy that emphasizes having more low-level, fast-running unit tests, fewer integration tests, and even fewer end-to-end tests. The Test Pyramid is a strategy that emphasizes different layers of testing:

    • Unit Tests – Fast and numerous tests that check small parts of the application (base of the pyramid).

    • Integration Tests – Fewer than unit tests; check if different modules or services work together.

    • End-to-End (E2E) Tests – Simulate user flows through the whole system. Expensive to run, so fewer in number.

  • This structure helps developers balance speed, reliability, and cost of automated testing.

API Testing

  • Testing helps make sure that software works correctly, securely, and as expected across all parts — backend, frontend, and APIs.

  • API testing verifies that your application's backend services (REST, GraphQL, gRPC, etc.) behave as expected. API Testing Checks:

    • Correct responses (status codes, response body)

    • Error handling (e.g., 400, 404, 500)

    • Authentication & Authorization

    • Performance and load handling

    • Data validation and transformation

  • Types of API Testing

    • Unit Testing: Tests individual API functions in isolation.

      • Types of Unit Tests:

        • Solitary Unit Test: Uses mocks to isolate dependencies.

        • Sociable Unit Test: Allows real interactions with some dependencies (e.g., calling another class method).

      • Example: Testing a calculateDiscount() function using Jest in Node.js.

    • Integration Testing: Checks how APIs work together or connect with databases.

      • Example: A product listing API fetching data from MongoDB.

    • Functional Testing: Checks if the API returns the correct result for given input.

      • Example: Testing if /cart/add API adds an item correctly.

    • Security Testing: Ensures unauthorized users can't access secure endpoints.

      • Example: Checking if admin-only APIs are protected.

    • Performance Testing: Measures speed, load behavior, and response time.

      • Tools: JMeter, k6

    • End-to-End Testing(E2E): Validates complete workflows involving multiple APIs to ensure overall system behavior.

      • Tools: Cypress

      • Example: User signs in → adds item to cart → places order.

    • Regression Testing: Checks that new code changes do not break existing API functionality.

    • Contract Testing: Ensures communication between services (like frontend and backend) follows a fixed agreement (contract).

      • If the backend changes its API response format, contract testing will catch the break.

      • Example: Verifying that the /user/profile API still returns name and email as expected by the frontend.

      • Tool: Pact(for contract testing)

      • Types of Contract Testing:

        • Consumer-Driven Contract Testing: Consumer (client app) defines expectations.

          • Providers (API service) must meet those.

          • Reduces risk of breaking changes from the provider.

          • Tools: Pact, Spring Cloud Contract

        • Provider-Driven Contract Testing: Provider defines the rules clients must follow.

          • Often documented in OpenAPI (Swagger) formats.

    • Tools: Postman, Insomnia, Newman, REST Assured(for Java), SuperTest,Jest(for Node.js testing)

Frontend Testing

  • Frontend testing validates UI behavior, layout, interactivity, and compatibility across platforms and devices.

  • Unit Testing: Tests individual components (e.g., buttons, forms)

    • Example:

      • Testing if a button shows "Loading.." after being clicked.

      • Test if a LoginForm validates inputs and calls the login function on submit.

    • Tool: Jest, Mocha, JUnit (Java), React Testing Library

  • Integration Testing: Verifies connected components work together

    • Example: Login form → AuthService → Dashboard redirect

    • Tool: Postman, Supertest

  • Visual Regression Testing: Compares screenshots before and after code changes.

    • Detects unexpected UI changes (e.g., a misplaced button after a CSS tweak).

    • Helps preserve UI consistency.

    • Tools: Percy, Chromatic,Storybook

  • Cross-Browser Testing: Ensures the website looks and works the same on different browsers.

    • Tools: BrowserStack, Sauce Labs

    • Example: A button works in Chrome but not in Safari due to an unsupported CSS property.

  • Acceptance Testing(E2E): Checks if the whole system meets user needs and requirements.

    • Simulates full user journey

    • Tool: Cypress, Playwright, Selenium

    • Example:

      • User signs in → adds product → checks out

      • Test if a user can successfully reset their password through the flow defined by the business.

  • Visual and Layout Consistency Checks: Detects differences in fonts, colors, alignment, and responsive design across browsers.

    • Tool: Percy, Storybook

  • Performance Testing: Measures load times and responsiveness on different platforms.

  • Responsive Testing: Verifies UI adapts to screen sizes

    • Example: Verifying that a three-column layout collapses into one column on mobile.

    • Tools: Chrome DevTools (Device Mode), LT Browser

  • Accessibility Testing (a11y): Ensures usability for disabled users

    • Example: Ensuring a user can fill and submit a form using only the keyboard.

    • Tools: axe DevTools, Lighthouse, WAVE, NVDA (screen reader)

Automated API testing using tools like Dredd and Schemathesis

  • To ensure correct behaviour, We could write unit tests manually for each endpoint, but that takes time and may miss edge cases. Tools like Dredd and Schemathesis automate this process using your API documentation/specification (like an OpenAPI YAML file).

  • Dredd: Automatically runs tests against REST APIs based on your OpenAPI/Swagger spec.

    • It ensures that endpoints conform to the documented contract.

    • Example: Create a simple Express.js API, write its OpenAPI spec, and test it using Dredd.

    • Workflow:

      • Reads OpenAPI YAML file

      • Calls every endpoint

      • Compares actual response with expected spec

      • Fails the test if anything doesn’t match

    • Problem With Default Dredd Behavior: It lacks dynamic behavior. For example:

      • If an endpoint uses path parameters like /orders/{order_id}, Dredd cannot automatically generate a valid order_id.

      • It expects hardcoded values like "order_id": "123", which must exist in the system before the test runs.

      • This leads to brittle tests, heavily dependent on fixtures (preloaded test data).

    • Dredd Hooks: Hooks allow customizing Dredd’s behavior dynamically during test execution:

      • Create resources (e.g., POST /orders) at runtime.

      • Save returned resource IDs.

      • Reuse those IDs in subsequent API calls (e.g., GET, PUT, DELETE).

      • Clean up after the test finishes.

    • Strength: Automatically tests all documented API endpoints (like /orders) using valid ("happy path") payloads.

    • Limitation: Only tests happy paths, doesn’t handle invalid or edge cases well

      • Example:

        • Missing required fields.

        • Invalid enum values.

        • Incorrect data types.

      • To go beyond the happy path, use Schemathesis, which leverages a more exhaustive testing technique called Property-Based Testing.

  • Schemathesis: It is a Python-based CLI tool. It's language-agnostic in terms of API testing (you can test Node, Java, Python APIs), but the tool itself runs in Python.

    • Uses property-based testing to test APIs more thoroughly than traditional tools like Dredd.

    • Internally, it uses the Hypothesis library to generate randomized and edge-case-oriented test cases based on your OpenAPI spec.

    • Limitation: Requires OpenAPI spec and Python environment

Test-Driven Development (TDD) Cycle

  • In TDD, we write tests before the code. The cycle:

    • Write a test: Based on a feature or requirement.

    • Run the test: It should fail (red phase).

    • Write minimal code: Just enough to pass the test.

    • Run tests again: Should pass (green phase).

    • Refactor: Clean up code without changing functionality.

Four Phases of a Test

  • Setup – Prepare input or environment

  • Exercise – Call the function/method

  • Verify – Assert the expected output

  • Teardown – Clean up (e.g., close DB, delete temp files)

  • Code Example:

    • let user;
      beforeEach(() => {
          user = { name: 'Alice', age: 25 }; // Setup
      });
      test('updates user age correctly', () => {
          updateUserAge(user, 30); // Exercise
          expect(user.age).toBe(30); // Verify
      });
      afterEach(() => {
          user = null; // Teardown
      });

Test Coverage

  • Test coverage tells how much of your code is tested. It Measures:

    • What lines, functions, and branches are tested.

    • What parts of the code are not touched by any test.

    • Helps identify untested or dead code.

    • Ensures critical paths in the application are verified.

    • Helps we write more complete tests

    • More coverage = more confidence in your code.

  • Example in Node.js

    • Math.js

      • // math.js
        function add(a, b) {
            return a + b;
        }
        function unusedFunction() {
            return 42;
        }
    • math.test.js

      • // math.test.js
        test('adds numbers', () => {
            expect(add(2, 3)).toBe(5);
        });
    • Only add() is covered, not unusedFunction(). That’s why we don’t get 100% coverage.

Test Doubles (Simulating External Dependencies)

  • Test Doubles are fake objects used in testing to simulate real ones like DBs or APIs.

  • Types of Test Doubles:

    • Test Spy: Tracks if a function was called, how often, and with what arguments

      • Does not change actual behavior

      • Example: Spy on emailService.send() to check usage

    • Test Stub: Replaces actual behavior with predefined output

      • Prevents real API/DB access

      • Example: Stub DB call to return fake user data

    • Mock: A full fake object that simulates an external service.

      • Mocks both behavior and interaction. Useful when:

        • We want to simulate a service (like sending an email)

        • AND check if the service was used properly (method called, arguments correct)

      • Mock Example: Sending a Welcome Email

        • Real code:

          • function sendWelcomeEmail(user, emailService) {
            emailService.send(user.email, 'Welcome!');
            }
        • Mock Test:

          • const mockEmailService = {
                send: jest.fn()
            };
            const user = { email: 'manish@example.com' };
            sendWelcomeEmail(user, mockEmailService);
            // Simulate and verify
            expect(mockEmailService.send).toHaveBeenCalledWith('manish@example.com', 'Welcome!');

Jest – JavaScript Testing Framework

  • Jest is a powerful testing framework especially popular in React and Node.js projects.

  • Components of Jest:

    • Test Runner: Finds and runs test files.

    • Assertion Library: Validates test expectations (expect()).

    • Mocking Framework: Mocks external dependencies.

      • Mocks help simulate APIs, databases, or services so that unit tests can run in isolation.

  • Example 1:

    • describe('Login Function', () => {
          test('returns true for correct password', () => {
              expect(checkPassword('1234')).toBe(true);
          });
      });
  • Example 2:

    • describe('Addition', () => {
          test('adds numbers correctly', () => {
              expect(1 + 2).toBe(3);
          });
      });
  • describe() groups tests.

  • test() or it() defines individual test cases.

  • expect() asserts expected behavior.

React Testing Library (RTL)

  • A lightweight library for testing React components.

  • Key Concepts:

    • RTL focuses on testing React components the way users interact with them, not how they’re implemented.

    • Focuses on behavior, not implementation.

  • RTL Features:

    • Queries by role, text, or label: Simulates real user behavior.

    • Encourages better accessibility because it interacts with accessible labels.

      • Encourages accessible markup (labels, roles).

    • Avoids testing implementation details (e.g., internal state).

  • Example:

    • const loginBtn = screen.getByRole('button', { name: /login/i });

    • fireEvent.click(loginBtn);

Last updated