{"id":30743615,"url":"https://github.com/doytch/rtl-apollo-testing","last_synced_at":"2026-05-04T08:39:07.452Z","repository":{"id":311309711,"uuid":"1043260051","full_name":"doytch/rtl-apollo-testing","owner":"doytch","description":"Demonstrating approaches for testing React components that integrate with Apollo Client for GraphQL operations.","archived":false,"fork":false,"pushed_at":"2025-08-23T18:58:08.000Z","size":46,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T08:38:44.359Z","etag":null,"topics":["apollo","graphql","react","react-testing-library","testing","testing-library"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/doytch.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2025-08-23T13:29:58.000Z","updated_at":"2025-08-23T18:58:11.000Z","dependencies_parsed_at":"2025-08-24T06:45:43.440Z","dependency_job_id":"6af83e39-aab8-474e-9dc3-de52a3d9c4a6","html_url":"https://github.com/doytch/rtl-apollo-testing","commit_stats":null,"previous_names":["doytch/rtl-apollo-testing"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/doytch/rtl-apollo-testing","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doytch%2Frtl-apollo-testing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doytch%2Frtl-apollo-testing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doytch%2Frtl-apollo-testing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doytch%2Frtl-apollo-testing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/doytch","download_url":"https://codeload.github.com/doytch/rtl-apollo-testing/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/doytch%2Frtl-apollo-testing/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32600967,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apollo","graphql","react","react-testing-library","testing","testing-library"],"created_at":"2025-09-04T02:44:42.408Z","updated_at":"2026-05-04T08:39:07.428Z","avatar_url":"https://github.com/doytch.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# React Testing Library + Apollo Client Testing\n\nThis project demonstrates a comprehensive testing strategy for React components that use Apollo Client for GraphQL operations. The approach focuses on testing components in isolation while ensuring full coverage of network interactions.\n\nThe reader should already be familiar with React Testing Library and Apollo idioms. For simplicity, I've ignored Apollo type generation (I assume you have that sorted) and define my types by hand.\n\nAll code in this readme is in the project and runnable. I recommend exercising it and seeing how different modifications to the components will make the tests red.\n\n## Table of Contents\n\n- [Components Overview](#components-overview)\n- [Drawing Seams with Mocking](#drawing-seams-with-mocking)\n- [Testing Query Execution](#testing-query-execution)\n- [Testing Mutation Execution](#testing-mutation-execution)\n- [Testing Data Refetching](#testing-data-refetching)\n- [Key Testing Principles](#key-testing-principles)\n\n## Components Overview\n\nBoth of the the components here do a decent amount of things. In real-world scenarios, doing network-access _and_ DOM rendering might be a bit too much if we're playing by SOLID-ish principles and trying to write maintainable tests.\n\nThose scenarios could split each of these components into smart/dumb components, or use more presentation components, etc. The Dear Reader is more than capable of imagining what that would look like.\n\n### Users Component\n\nThe `Users` component is a component that:\n\n- Fetches and displays a list of users\n- Provides a button to open a user creation dialog\n- Handles loading and error states\n- Refetches data after successful user creation\n\n### CreateUserDialog Component\n\nThe `CreateUserDialog` component is a component that:\n\n- Manages its own form state\n- Handles the `CREATE_USER` mutation\n- Provides success/error callbacks to parent components\n\n## Drawing Seams with Mocking\n\nDO NOT mock every child component. This goes against the Testing Library principle of [\"the more your tests resemble the way your software is used, the more confidence they can give you.\"](https://testing-library.com) However, when a child component becomes complex enough to have its own substantial responsibilities (network calls, complex state management, etc.), it becomes a good candidate for mocking because:\n\n- It creates a natural seam between different areas of responsibility\n- It allows you to focus each test on a specific concern\n- It keeps tests fast and maintainable\n\nThe `CreateUserDialog` component (especially a realistic implementation) is complex and has its own network responsibilities. Rather than testing the entire integration, we draw a **seam** between the components by mocking the dialog.\n\n### Mock Implementation\n\nIn `Users.test.tsx`, we mock the `CreateUserDialog` component:\n\n```typescript\nvitest.mock(\"./CreateUserDialog\", () =\u003e ({\n  default: (props: ComponentProps\u003ctypeof CreateUserDialog\u003e) =\u003e {\n    if (!props.isOpen) return null;\n\n    return (\n      \u003cdialog open={props.isOpen} onClose={props.onClose}\u003e\n        \u003cbutton\n          data-testid=\"mock-create-user-button\"\n          onClick={() =\u003e {\n            props.onSuccess({\n              id: \"1\",\n              name: \"John Doe\",\n              email: \"john.doe@example.com\",\n              role: \"admin\",\n            });\n            props.onClose();\n          }}\n        \u003e\n          Create\n        \u003c/button\u003e\n      \u003c/dialog\u003e\n    );\n  },\n}));\n```\n\nWe delegate all implementation details of the dialog (eg, does `onSuccess` and `onClose` get called at the right time?) to its own tests.\n\nThis simple mock:\n\n- Simulates the dialog's open/close behavior\n- Provides a simple button that triggers the success callback\n- Uses test IDs to clearly indicate it's a mock component\n- Allows us to test the parent component's integration logic without the complexity of the real dialog.\n\n## Testing Query Execution\n\nWe use Apollo Client's `MockedProvider` to verify that the correct queries are executed. For convenience, we're importing the query from the component under test, but a hardliner could write it out explicitly.\n\n```typescript\ntest(\"renders users if there are users\", async () =\u003e {\n  render(\n    \u003cMockedProvider\n      mocks={[\n        {\n          request: { query: USERS_QUERY },\n          result: { data: { users: [{ id: \"1\", name: \"John Doe\" }] } },\n        },\n      ]}\n    \u003e\n      \u003cUsers /\u003e\n    \u003c/MockedProvider\u003e\n  );\n\n  expect(await screen.findByText(\"John Doe\")).toBeVisible();\n});\n```\n\n## Testing Mutation Execution\n\nFor the `CreateUserDialog` component, we test that mutations are called with the correct variables. A key technique here is using the **function form** of `MockedResponse['result']`:\n\n```typescript\ntest(\"calls the createUser mutation when the form is submitted\", async () =\u003e {\n  // Set up a mock mutation result. This uses the function form of MockedResponse['result']\n  // that returns a mock result. This function is called by Apollo on-demand only when\n  // the response is needed. This allows us to assert that a specific query/mutation\n  // was actually called and the response was requested.\n  const onMutationResult = vitest.fn(() =\u003e ({\n    data: {\n      createUser: {\n        id: \"1\",\n        name: \"John Doe\",\n        email: \"john.doe@example.com\",\n        role: \"admin\",\n      },\n    },\n  }));\n\n  render(\n    \u003cMockedProvider\n      mocks={[\n        {\n          request: {\n            query: CREATE_USER_MUTATION,\n            variables: {\n              name: \"John Doe\",\n              email: \"john.doe@example.com\",\n              role: \"admin\",\n            },\n          },\n          result: onMutationResult, // Function form, not a plain object\n        },\n      ]}\n    \u003e\n      \u003cCreateUserDialog /\u003e\n    \u003c/MockedProvider\u003e\n  );\n\n  // ...\n\n  await waitFor(() =\u003e expect(onMutationResult).toHaveBeenCalled());\n});\n```\n\n### Why Use Function Form?\n\nThe function form of `MockedResponse['result']` is crucial because the function is only called when Apollo actually needs the response, not when the mock is defined. Combined with the appropriate assert (`toHaveBeenCalled`), this gives us a guarantee that we went \"onto the wire\" and called that specific mutation with those specific variables.\n\n### Testing Error Scenarios\n\nWe also test error handling by providing error responses in our mocks:\n\n```typescript\ntest(\"calls onError when user creation fails\", async () =\u003e {\n  const onError = vitest.fn();\n\n  render(\n    \u003cMockedProvider\n      mocks={[\n        {\n          request: {\n            query: CREATE_USER_MUTATION,\n            variables: {\n              /* ... */\n            },\n          },\n          error: new Error(\"User creation failed\"),\n        },\n      ]}\n    \u003e\n      \u003cCreateUserDialog onError={onError} /\u003e\n    \u003c/MockedProvider\u003e\n  );\n\n  // Submit form and verify error callback\n  await userEvent.click(screen.getByRole(\"button\", { name: \"Create\" }));\n  await waitFor(() =\u003e\n    expect(onError).toHaveBeenCalledWith(new Error(\"User creation failed\"))\n  );\n});\n```\n\n## Testing Data Refetching\n\n### Refetch Strategy\n\nThe `Users` component refetches data after successful user creation by calling the `refetch()` function from Apollo's `useQuery` hook:\n\n```typescript\n\u003cCreateUserDialog\n  isOpen={isCreateUserDialogOpen}\n  onClose={() =\u003e setIsCreateUserDialogOpen(false)}\n  onSuccess={() =\u003e {\n    setIsCreateUserDialogOpen(false);\n    refetch(); // This triggers a new query execution\n  }}\n  onError={(error) =\u003e {\n    console.error(error);\n  }}\n/\u003e\n```\n\nYou could also use refetch sets if it makes sense in your scenario and this testing approach will work the same.\n\nHowever, if the dialog here was doing direct Apollo cache modification, this strategy would not work. Testing cache modification becomes more complicated; reliant on Apollo; and impossible to test across dependent components like this. This is why I tend to avoid that strategy and stick with simple, boring `refetch()` if possible.\n\n### Testing Refetch Behavior\n\nWe test this by providing multiple mock responses for the same query:\n\n```typescript\ntest(\"refetches users after creating a user\", async () =\u003e {\n  render(\n    \u003cMockedProvider\n      mocks={[\n        // Initial query returns no users\n        {\n          request: { query: USERS_QUERY },\n          result: { data: { users: [] } },\n        },\n        // Refetch query returns a user\n        {\n          request: { query: USERS_QUERY },\n          result: { data: { users: [{ id: \"1\", name: \"John Doe\" }] } },\n        },\n      ]}\n    \u003e\n      \u003cUsers /\u003e\n    \u003c/MockedProvider\u003e\n  );\n\n  // Initially shows \"No users\"\n  expect(await screen.findByText(\"No users\")).toBeVisible();\n\n  // Open dialog and create user\n  await userEvent.click(screen.getByRole(\"button\", { name: \"Create User\" }));\n  await userEvent.click(screen.getByTestId(\"mock-create-user-button\"));\n\n  // Verify refetch occurred and new data is displayed\n  expect(await screen.findByText(\"John Doe\")).toBeVisible();\n});\n```\n\nThis approach ensures that:\n\n1. The initial query executes correctly\n2. The refetch is triggered after user creation\n3. The UI updates with the new data\n4. The dialog closes properly\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoytch%2Frtl-apollo-testing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdoytch%2Frtl-apollo-testing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdoytch%2Frtl-apollo-testing/lists"}