83. Integration Testing for Client and Server
Integration Testing for Client and Server
In today’s digital era, modern web applications are almost always built on a distributed architecture: there is a client side (frontend) and a server side (backend), each of which is tested thoroughly and separately through unit testing. However, the “real” problems are often only discovered when these two parts interact. This is why Integration Testing—especially the kind that verifies the integration between client and server—is a crucial stage before an application is launched to users.
In this article, I will discuss integration testing for client-server applications, complete with real implementation examples, scenario simulations, and several best practices commonly used in the industry.
What Is Integration Testing Between Client and Server?
Integration Testing for client and server is the process of testing the communication and interaction between a frontend application (client) and a backend application (server). The goal is to ensure that the API contract, the data sent and received, error handling, and the user flow all work as expected. This differs from unit testing, which focuses on a single function within a limited scope.
Testing Coverage
| Integration Type | Example Cases |
|---|---|
| API Endpoint | Fetch user data, create order, login/register |
| Error Handling & Validation | 401, 422, 500 error responses, validation errors |
| Data Serialization/Deserialization | JSON response → Object/Model on the frontend |
| Auth & Session | Token expired, role-based access, refresh token |
| Data Contract (Contract Testing) | Fields, data types, JSON anatomy consistent with the docs |
Integration Testing Flow: An Overview
Let’s look at an overview of the flow with the following mermaid diagram:
sequenceDiagram
participant Frontend
participant Backend
participant MockServer
Frontend->>Backend: Kirim request (Contoh: POST /login)
Backend-->>Frontend: Response (success/error)
rect rgba(0, 210, 255, 0.08)
note over Frontend, Backend: Selama testing, Backend bisa direplace MockServer
Frontend->>MockServer: Kirim request (POST /login)
MockServer-->>Frontend: Response mock (simulasi)
end
Explanation:
During integration testing, the client can be tested against the real backend or against a mock server (a simulated API). The mock approach gives you the flexibility to create positive and negative cases, as well as to isolate errors.
Case Study: A Simple Notes Application
Let’s look at a concrete example of integration testing with a JavaScript stack (React as the client and Express.js as the server).
1. Backend API Endpoint (Express.js)
1// server/routes/notes.js
2const express = require('express');
3const router = express.Router();
4
5let notes = [{id: 1, text: "Halo Dunia"}];
6
7router.get('/notes', (req, res) => {
8 res.json(notes);
9});
10
11router.post('/notes', (req, res) => {
12 if (!req.body.text) return res.status(400).json({ error: "Text is required" });
13 const newNote = { id: Date.now(), text: req.body.text };
14 notes.push(newNote);
15 res.status(201).json(newNote);
16});
17
18module.exports = router;2. Frontend Client (React, using the Fetch API)
1// src/apiClient.js
2export async function getNotes() {
3 const response = await fetch('/api/notes');
4 if (!response.ok) throw new Error('Failed fetch notes');
5 return await response.json();
6}
7export async function addNote(text) {
8 const response = await fetch('/api/notes', {
9 method: 'POST',
10 headers: { 'Content-Type': 'application/json' },
11 body: JSON.stringify({ text }),
12 });
13 if (!response.ok) throw new Error('Failed add note');
14 return await response.json();
15}Integration Testing: Tools and Methods
Here are several popular tools for performing client-server integration testing:
- Supertest (Node.js): Tests backend API endpoints directly, without having to deploy or spin up the server over HTTP.
- Jest + Testing Library (React): Simulates user events and validates the result of API integration on the frontend.
- MSW (Mock Service Worker): Intercepts fetch/XHR on the client side to mock REST/GraphQL APIs.
- Cypress / Playwright: End-to-end integration at the UI level.
Table: Comparison of Integration Testing Tools
| Tools | Testing Level | Mock vs Real Server | Advantages |
|---|---|---|---|
| Supertest | Backend endpoint | No mock (calls the handler directly) | Fast, isolates the backend |
| MSW | Frontend API Client | Can Mock | Simulates various scenarios, easy to set up |
| Cypress | Fullstack/E2E | Real/Mock | Real browser, realistic scenarios |
Integration Test Examples
A. Testing an API Endpoint (Backend): Supertest
1// server/__tests__/notes.test.js
2const request = require('supertest');
3const app = require('../app');
4
5describe('Notes API', () => {
6 it('GET /notes should return array of notes', async () => {
7 const res = await request(app).get('/api/notes');
8 expect(res.statusCode).toBe(200);
9 expect(Array.isArray(res.body)).toBe(true);
10 expect(res.body[0]).toHaveProperty('text');
11 });
12
13 it('POST /notes with no text should return 400', async () => {
14 const res = await request(app).post('/api/notes').send({});
15 expect(res.statusCode).toBe(400);
16 expect(res.body).toHaveProperty('error');
17 });
18});B. Testing API Client Integration on the Frontend: MSW + Testing Library
Setting Up the Mock Handler:
1// src/mocks/handlers.js
2import { rest } from 'msw';
3
4export const handlers = [
5 rest.get('/api/notes', (req, res, ctx) => {
6 return res(ctx.json([{id: 1, text: 'Catatan mock'}]));
7 }),
8 rest.post('/api/notes', (req, res, ctx) => {
9 const { text } = req.body;
10 if (!text) return res(ctx.status(400), ctx.json({error: "Text is required"}));
11 return res(ctx.status(201), ctx.json({id: 2, text}));
12 })
13];Frontend Test Simulation:
1// src/__tests__/NoteApp.test.js
2import { render, screen, fireEvent, waitFor } from '@testing-library/react';
3import NoteApp from '../NoteApp';
4
5test('User dapat melihat dan menambah catatan', async () => {
6 render(<NoteApp />);
7
8 // Pastikan note mock tampil
9 expect(screen.getByText(/Catatan mock/i)).toBeInTheDocument();
10
11 // Tambah note baru
12 fireEvent.change(screen.getByPlaceholderText(/Tulis catatan/i), {
13 target: { value: 'Catatan kedua' }
14 });
15 fireEvent.click(screen.getByText(/Tambah/i));
16
17 await waitFor(() =>
18 expect(screen.getByText(/Catatan kedua/i)).toBeInTheDocument()
19 );
20});Simulation: Testing Error Handling
Testing isn’t only about checking the “happy path”—it’s also about errors. Here is a simulation of an error response (for example: the server returns a 500/error field):
1rest.post('/api/notes', (req, res, ctx) => {
2 return res(ctx.status(500), ctx.json({ error: "Server error" }));
3});Then, the test on the client can display an error notification, and so on.
Best Practices for Client-Server Integration Testing
- Mocked & Real API: Start with a mocked API (for example: MSW), then move on to the real backend or a staging API for an end-to-end “smoke test”.
- Contract Testing: Validate the data schema, for example with pact.io .
- Pipeline Automation: Integrate the tests into CI/CD (GitHub Actions, Jenkins, etc.).
- Data Isolation: Use a testing database (for example SQLite/mockDB) to avoid corrupting production data.
Conclusion
Integration testing between client and server is an essential foundation in modern web development. With solid integration tests, we minimize bugs in API integration, reduce regressions, and increase our confidence before the application goes to production. Use the right tools to suit your application’s needs. Happy testing!
Further Reading:
Have you already applied integration testing in your client-server project? Or do you have an interesting experience involving strange errors at the client/backend boundary? Feel free to discuss or share in the comments section! 🚀