End-to-End Integration Testing
Objective
Create comprehensive integration test suite that validates the entire stack from database through mobile/web clients.
Background
Integration tests ensure all components work together: PostgreSQL → PostGraphile → Apollo bindings → ReasonReact components.
Tasks
-
Set up test database with Docker -
Create test data fixtures and seeds -
Build GraphQL integration tests -
Test Apollo bindings with real queries -
Create component integration tests -
Add authentication flow tests -
Test mobile/web parity -
Set up CI test pipeline -
Add performance benchmarks
Test Structure
tests/
├── integration/
│ ├── database/
│ │ ├── DatabaseTest.re
│ │ └── fixtures/
│ ├── api/
│ │ ├── GraphQLTest.re
│ │ ├── AuthTest.re
│ │ └── queries/
│ ├── web/
│ │ ├── ComponentTest.re
│ │ └── E2ETest.re
│ └── mobile/
│ ├── ScreenTest.re
│ └── NavigationTest.re
├── fixtures/
│ ├── users.sql
│ ├── projects.sql
│ └── tasks.sql
├── docker-compose.test.yml
└── dune-project
Database Integration Tests
(* tests/integration/database/DatabaseTest.re *)
open Test_framework;
let testDatabaseConnection = () =>
describe("Database Connection", () => {
test("connects to test database", async () => {
let%Async result = Database.connect(~testMode=true, ());
result |> Result.fold(
error => fail("Failed to connect: " ++ error),
_connection => pass
);
});
test("creates user with RLS", async () => {
let%Async user = User.create(~email="test@example.com", ~name="Test User", ());
user |> Result.fold(
error => fail("Failed to create user: " ++ error),
createdUser => {
expect(createdUser.email) |> toEqual("test@example.com");
}
);
});
});
GraphQL Integration Tests
(* tests/integration/api/GraphQLTest.re *)
open MelangeApollo;
module TestQueries = {
module GetUsers = [%graphql {|
query GetUsers {
users {
id
email
name
}
}
|}];
module CreateTask = [%graphql {|
mutation CreateTask($input: CreateTaskInput\!) {
createTask(input: $input) {
task {
id
title
status
}
}
}
|}];
};
let testClient = createClient({
uri: "http://localhost:5001/graphql", // Test server
headers: [("Authorization", "Bearer " ++ TestAuth.getToken())],
});
let testGraphQLQueries = () =>
describe("GraphQL Integration", () => {
test("fetches users", async () => {
let%Async result = query(testClient, TestQueries.GetUsers.make());
result |> Result.fold(
error => fail("Query failed: " ++ error##message),
data => {
expect(Array.length(data##users)) |> toBeGreaterThan(0);
}
);
});
test("creates task with mutation", async () => {
let input = {"title": "Test Task", "description": None, "projectId": "1"};
let%Async result = mutate(testClient, TestQueries.CreateTask.make(~input, ()));
result |> Result.fold(
error => fail("Mutation failed: " ++ error##message),
data => {
expect(data##createTask##task##title) |> toEqual("Test Task");
}
);
});
});
Component Integration Tests
(* tests/integration/web/ComponentTest.re *)
open TestingLibrary;
open MelangeApollo;
let mockClient = createMockClient([
(TestQueries.GetTasks.make(~projectId="1", ()),
Ok({"tasks": [{"id": "1", "title": "Test Task", "status": "TODO"}]}))
]);
let testTaskListComponent = () =>
describe("TaskList Component", () => {
test("renders loading state", () => {
let component = render(
<ApolloProvider client=mockClient>
<TaskList projectId="1" />
</ApolloProvider>
);
expect(getByText(component, "Loading tasks...")) |> toBeInTheDocument();
});
test("renders tasks from GraphQL", async () => {
let component = render(
<ApolloProvider client=mockClient>
<TaskList projectId="1" />
</ApolloProvider>
);
let%Async _element = waitFor(() => getByText(component, "Test Task"));
expect(getByText(component, "Test Task")) |> toBeInTheDocument();
});
});
Authentication Flow Tests
(* tests/integration/api/AuthTest.re *)
let testAuthenticationFlow = () =>
describe("Authentication Flow", () => {
test("signup creates user and returns JWT", async () => {
let credentials = {"email": "new@example.com", "password": "secure123"};
let%Async result = Auth.signup(credentials);
result |> Result.fold(
error => fail("Signup failed: " ++ error),
response => {
expect(Option.isSome(response.token)) |> toBe(true);
expect(response.user.email) |> toEqual("new@example.com");
}
);
});
test("login with valid credentials", async () => {
let credentials = {"email": "test@example.com", "password": "password123"};
let%Async result = Auth.login(credentials);
result |> Result.fold(
error => fail("Login failed: " ++ error),
response => {
expect(Option.isSome(response.token)) |> toBe(true);
}
);
});
test("protected query requires authentication", async () => {
let unauthenticatedClient = createClient({uri: "http://localhost:5001/graphql", headers: []});
let%Async result = query(unauthenticatedClient, TestQueries.GetCurrentUser.make());
result |> Result.fold(
error => expect(error##message) |> toContain("Unauthorized"),
_data => fail("Should have failed without auth")
);
});
});
Docker Test Environment
# docker-compose.test.yml
version: '3.8'
services:
postgres-test:
image: postgres:15-alpine
environment:
POSTGRES_DB: melange_mvp_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5433:5432"
volumes:
- ./tests/fixtures:/docker-entrypoint-initdb.d
tmpfs:
- /var/lib/postgresql/data
api-test:
build: ./packages/api-server
environment:
DATABASE_URL: postgres://postgres:postgres@postgres-test:5432/melange_mvp_test
JWT_SECRET: test-secret
NODE_ENV: test
ports:
- "5001:5000"
depends_on:
- postgres-test
Dune Test Configuration
(melange.emit
(target test)
(libraries
melange_shared
melange_apollo
melange-jest
melange-testing-library
reason-react
relude)
(preprocess
(pps melange.ppx reason-react-ppx graphql_ppx))
(flags (:standard -open Relude.Globals)))
(rule
(target run_tests.js)
(deps (alias test))
(action (run node %{target})))
CI Pipeline Integration
# .gitlab-ci.yml additions
integration-tests:
stage: test
services:
- postgres:15-alpine
variables:
POSTGRES_DB: melange_mvp_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
script:
- docker-compose -f docker-compose.test.yml up -d
- sleep 10 # Wait for services
- dune build @test
- npm run test:integration
after_script:
- docker-compose -f docker-compose.test.yml down
Performance Benchmarks
(* tests/integration/performance/BenchmarkTest.re *)
let testPerformance = () =>
describe("Performance Benchmarks", () => {
test("GraphQL query response time", async () => {
let startTime = Performance.now();
let%Async _result = query(testClient, TestQueries.GetUsers.make());
let endTime = Performance.now();
let duration = endTime -. startTime;
expect(duration) |> toBeLessThan(100.0); // < 100ms
});
test("component render time", () => {
let startTime = Performance.now();
let _component = render(<TaskList projectId="1" />);
let endTime = Performance.now();
let duration = endTime -. startTime;
expect(duration) |> toBeLessThan(50.0); // < 50ms
});
});
Acceptance Criteria
-
Database integration tests pass -
GraphQL API tests pass -
Authentication flow tests pass -
Component integration tests pass -
Mobile/web parity validated -
Performance benchmarks within limits -
CI pipeline runs tests automatically -
Test coverage reports generated
Test Scripts
{
"scripts": {
"test:integration": "dune build @test && node _build/default/tests/integration/run_tests.js",
"test:watch": "dune build @test --watch",
"test:ci": "docker-compose -f docker-compose.test.yml up --abort-on-container-exit"
}
}
🟡 Medium
Priority: Ensures system reliability and catches regressions.
Estimated Effort: 2-3 days
Dependencies
CI Validation
-
All integration tests pass -
Performance benchmarks pass -
No flaky tests -
Tests run in CI environment