Skip to content

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"
  }
}

Priority: 🟡 Medium

Ensures system reliability and catches regressions.

Estimated Effort: 2-3 days

Dependencies

  • #37 Database Foundation
  • #38 PostGraphile API
  • #36 Apollo bindings
  • #41 Web Frontend
  • #42 Mobile App

CI Validation

  • All integration tests pass
  • Performance benchmarks pass
  • No flaky tests
  • Tests run in CI environment