-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #176 from msqd/doc_frontend
[READY] Doc frontend testing
- Loading branch information
Showing
6 changed files
with
404 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -9,3 +9,4 @@ Development | |
changelogs/index | ||
roadmap | ||
ideas | ||
testing/index |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
End-to-End Tests with Playwright | ||
=========================== | ||
|
||
Playwright is a Node.js library for automating browser tasks. In our project, we use Playwright for end-to-end (E2E) testing. E2E tests simulate real user scenarios by running tests in a real browser environment. | ||
|
||
Here's an example of a script in our `package.json` file that runs our E2E tests with Playwright: | ||
|
||
.. code-block:: json | ||
"scripts": { | ||
"test:browser": "playwright test" | ||
} | ||
To run our E2E tests, we use the command `pnpm run test:browser`. This command starts Playwright, which opens a new browser window and runs our E2E tests. | ||
|
||
|
||
Using Playwright for End-to-End Testing | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
In our project, we use Playwright for end-to-end testing to simulate real user interactions with our application. Playwright provides a high-level API to control headless or non-headless browsers, enabling us to automate browser tasks and test our application in real-world scenarios. | ||
|
||
.. code-block:: typescript | ||
import { test, expect, request } from "@playwright/test" | ||
test.beforeEach(async ({ page }) => { | ||
await page.goto("/transactions") | ||
await page.waitForFunction(() => document.body.innerText.includes("Endpoint")) | ||
}) | ||
test.describe("Transactions Page", () => { | ||
test("Interacting with the filter side bar", async ({ page }) => { | ||
const requestMethodButton = await page.$('span:has-text("Request Method")') | ||
const getLabel = await page.getByLabel("GET") | ||
expect(getLabel).toBeVisible() | ||
await requestMethodButton?.click() | ||
expect(getLabel).not.toBeVisible() | ||
const endpointButton = await page.getByText("Endpoint", { exact: true }) | ||
const endpoint1Label = await page.getByLabel("endpoint1") | ||
expect(endpoint1Label).toBeVisible() | ||
await endpointButton?.click() | ||
expect(endpoint1Label).not.toBeVisible() | ||
}) | ||
}) | ||
In this example, we use Playwright to test the interactions with the filter sidebar on the Transactions page. We navigate to the Transactions page before each test and wait for the page to load. In the test, we simulate user interactions with the filter sidebar, such as clicking on buttons and checking the visibility of elements. We use Playwright's `expect` function to assert the expected outcomes of these interactions. | ||
|
||
This approach allows us to ensure that our application behaves as expected when users interact with it, providing us with confidence in the quality of our application. | ||
|
||
|
||
Setting Up MSW in Development | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
In our project, we set up the Mock Service Worker (MSW) in development mode to mock API responses. This is done in the `main.tsx` file, where we conditionally import the MSW worker and start it if the application is running in development mode. | ||
|
||
.. code-block:: typescript | ||
// Enable mocking in development using msw server set up for the browser | ||
async function enableMocking() { | ||
if (process.env.NODE_ENV !== "development") { | ||
return | ||
} | ||
const { worker, http, HttpResponse } = await import("./tests/mocks/browser") | ||
// @ts-ignore | ||
// Propagate the worker and `http` references to be globally available. | ||
// This would allow to modify request handlers on runtime. | ||
window.msw = { | ||
worker, | ||
http, | ||
HttpResponse, | ||
} | ||
return worker.start() | ||
} | ||
In this function, we first check if the application is running in development mode. If it is, we dynamically import the MSW worker, `http`, and `HttpResponse` from our browser mocks. We then assign these to the `window.msw` object, making them globally available. This allows us to modify the request handlers at runtime, which is useful for overriding handlers in specific tests. Finally, we start the MSW worker, which begins intercepting network requests according to the predefined handlers. | ||
|
||
Overriding Handlers for Single Tests | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
In some cases, we might want to override the default handlers for a single test. We can do this by accessing the `worker` object on the `window` object and calling its `use` method with a new handler. | ||
|
||
.. code-block:: typescript | ||
test("Override msw worker for system dependencies", async ({ page }) => { | ||
// Test setup code here... | ||
await page.evaluate(() => { | ||
const { worker, http, HttpResponse } = window.msw | ||
worker.use( | ||
http.get("/api/system/dependencies", function override() { | ||
return HttpResponse.json({ python: ["pydantic", "tensorflow"] }) | ||
}), | ||
) | ||
}) | ||
// Test code here... | ||
}) | ||
In this test, we override the handler for GET requests to `/api/system/dependencies` to return a predefined JSON response. This allows us to control the data that our application receives from the API in this specific test. | ||
|
||
Running End-to-End Tests | ||
~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
To run our end-to-end tests, we use the command | ||
|
||
.. code-block:: bash | ||
pnpm run test:browser | ||
This command starts Playwright, which opens a new browser window and runs our E2E tests. We can also run a single test file by specifying the file path as an argument to the `test` command: | ||
|
||
.. code-block:: bash | ||
pnpm run test:browser transactions.spec.ts | ||
This command runs the tests in the `transactions.spec.ts` file using Playwright. | ||
|
||
By running our end-to-end tests, we can ensure that our application behaves as expected in real-world scenarios, providing us with confidence in the quality of our application. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
Frontend Testing | ||
================ | ||
|
||
First if you're in the root directory of the project let's move to the frontend directory: | ||
.. code-block:: bash | ||
cd frontend | ||
Our project uses Vitest and Playwright for frontend testing. | ||
|
||
|
||
Vitest | ||
------ | ||
|
||
Vitest is a JavaScript testing framework that is optimized for Vite. We use it for unit testing our JavaScript code. | ||
|
||
Here's an example of a basic Vitest test: | ||
|
||
.. code-block:: javascript | ||
import { test } from 'vitest'; | ||
test('Example test', () => { | ||
const result = 1 + 1; | ||
expect(result).toBe(2); | ||
}); | ||
For more information, see: | ||
|
||
.. toctree:: | ||
:maxdepth: 1 | ||
|
||
unit_tests | ||
|
||
|
||
|
||
|
||
Playwright | ||
---------- | ||
|
||
Playwright is a framework for end-to-end testing of web applications. It allows us to automate browser actions and assert on their results. | ||
|
||
Here's an example of a basic Playwright test: | ||
|
||
.. code-block:: javascript | ||
import { test, expect } from '@playwright/test'; | ||
test('Example test', async ({ page }) => { | ||
await page.goto('https://example.com'); | ||
const title = await page.title(); | ||
expect(title).toBe('Example Domain'); | ||
}); | ||
For more information, see: | ||
|
||
.. toctree:: | ||
:maxdepth: 1 | ||
|
||
e2e_tests | ||
|
||
Running Tests | ||
------------- | ||
|
||
To run all tests, use the following command: | ||
|
||
.. code-block:: bash | ||
pnpm test | ||
This will run both the Vitest unit tests and the Playwright end-to-end tests. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
Unit Testing with Vitest | ||
======================== | ||
|
||
Our project uses Vitest for unit testing. Vitest is a JavaScript testing framework that is optimized for Vite. | ||
|
||
|
||
Vitest Setup | ||
------------ | ||
|
||
Before we can test our smart components, we need to set up our testing environment. In our `vitest.setup.ts` file, we import several libraries and set up global variables to ensure our tests run correctly. | ||
|
||
.. code-block:: typescript | ||
import "@testing-library/jest-dom" | ||
import { server } from "./src/tests/mocks/node" | ||
global.ResizeObserver = require("resize-observer-polyfill"); | ||
global.requestAnimationFrame = fn => window.setTimeout(fn, 0); | ||
import { beforeAll, afterEach, afterAll } from 'vitest' | ||
beforeAll(() => { | ||
server.listen() | ||
}) | ||
afterEach(() => { | ||
server.resetHandlers() | ||
}) | ||
afterAll(() => { | ||
server.close() | ||
}) | ||
We import `@testing-library/jest-dom` to extend Jest's `expect` with matchers that are useful when testing DOM elements. We also import our mock server from `./src/tests/mocks/node`. | ||
|
||
We then set up a polyfill for `ResizeObserver`, which is not natively supported in Jest's environment. We also set up a mock for `requestAnimationFrame`, which is not available in Node.js where our tests run. | ||
|
||
Finally, we use `vitest`'s `beforeAll`, `afterEach`, and `afterAll` functions to start our mock server before all tests, reset any runtime request handlers between tests, and close the server after all tests. This ensures that our mock server is correctly set up for each test and that our tests do not interfere with each other. | ||
|
||
|
||
|
||
Writing Tests | ||
------------- | ||
|
||
Vitest tests are written in JavaScript files that end with `.test.js`. Each test file can contain multiple tests. | ||
|
||
Here's an example of a basic Vitest test for the HeadersTable component in our project: | ||
|
||
.. code-block:: javascript | ||
import { render, screen } from "@testing-library/react" | ||
import { describe, expect, test } from "vitest" | ||
import { HeadersTable } from "./HeadersTable" | ||
test("renders the correct headers", () => { | ||
render(<HeadersTable headers={{ "Test Header": "Test Value" }} />) | ||
const headerElement = screen.getByText(/Test Header/i) | ||
const valueElement = screen.getByText(/Test Value/i) | ||
expect(headerElement).toBeInTheDocument() | ||
expect(valueElement).toBeInTheDocument() | ||
}) | ||
In this example, the `test` function is used to define a test. The first argument is a string that describes what the test does. The second argument is a function that contains the test code. | ||
|
||
Snapshot Testing | ||
---------------- | ||
|
||
Snapshot tests are a way to test your UI component rendering. A snapshot represents the state of a UI component. On the first test run, a snapshot file is created that stores the rendered output of a component. On subsequent test runs, the rendered output is compared to the snapshot to check for differences. | ||
|
||
Here's an example of a snapshot test for the Facet component in our project: | ||
|
||
.. code-block:: javascript | ||
import { render } from "@testing-library/react" | ||
import { describe, expect, test } from "vitest" | ||
import { Facet } from "./Facet" | ||
describe("Facet", () => { | ||
test("renders without crashing", () => { | ||
const { container } = render(<Facet title="Test Title" name="test-name" type="checkboxes" meta={[]} />) | ||
expect(container).toMatchSnapshot() | ||
}) | ||
}) | ||
In this example, the `toMatchSnapshot` function is used to create a snapshot of the rendered `MyComponent`. If the rendering of `MyComponent` changes in the future, this test will fail. | ||
|
||
|
||
Testing Smart Components | ||
------------------------ | ||
|
||
Smart components are typically more complex to test than dumb components, as they are often tightly coupled with the application's state and business logic. They may also interact with services or APIs, which need to be mocked during testing. | ||
When testing smart components, we typically use a full render method that includes all child components. This allows us to test the component's behavior in the context of its data and state management. | ||
|
||
Mocking API Responses with MSW | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
We use the library `msw` (Mock Service Worker) to seamlessly mock API responses in our tests. This allows us to isolate our components from actual network requests and control the responses they receive. | ||
|
||
Here's an example of how we might use `msw` in a test: | ||
|
||
Here's an example of a test: | ||
|
||
.. code-block:: javascript | ||
import { renderWithClient } from "tests/utils" | ||
import { expect, it } from "vitest" | ||
import { MemoryRouter } from "react-router-dom" | ||
import { TransactionsListPage } from "./TransactionsListPage" | ||
it("renders well when the query is successful", async () => { | ||
const result = renderWithClient( | ||
<MemoryRouter> | ||
<TransactionsListPage /> | ||
</MemoryRouter>, | ||
) | ||
await result.findByText("0.06 seconds") | ||
expect(result.container).toMatchSnapshot() | ||
}) | ||
In this example, we use `MemoryRouter` to mock the router context for `TransactionsListPage`. | ||
|
||
In this example, we use the `renderWithClient` function to render our `SmartComponent` in the context of a `QueryClientProvider`, which allows it to use the `useQuery` hook from `react-query`. | ||
|
||
The `renderWithClient` function is defined as follows: | ||
|
||
.. code-block:: javascript | ||
import { render } from "@testing-library/react" | ||
import { QueryClient, QueryClientProvider } from "react-query" | ||
const createTestQueryClient = () => | ||
new QueryClient({ | ||
defaultOptions: { | ||
queries: { | ||
retry: false, | ||
}, | ||
}, | ||
}) | ||
export function renderWithClient(ui: React.ReactElement) { | ||
const testQueryClient = createTestQueryClient() | ||
const { rerender, ...result } = render(<QueryClientProvider client={testQueryClient}>{ui}</QueryClientProvider>) | ||
return { | ||
...result, | ||
rerender: (rerenderUi: React.ReactElement) => rerender(<QueryClientProvider client={testQueryClient}>{rerenderUi}</QueryClientProvider>), | ||
} | ||
} | ||
This function wraps the provided UI element in a `QueryClientProvider` with a test `QueryClient`, which allows us to test components that use `react-query` hooks. It also provides a `rerender` function that can be used to update the UI element during a test. | ||
|
||
|
||
One-Time Mocks for Single Tests | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
||
In some cases, you might want to set up a mock for a single test or change the mock response for a specific test. You can do this using `msw` and the `server.use` function. | ||
|
||
.. code-block:: javascript | ||
import { http } from 'msw' | ||
import { server } from "./src/tests/mocks/node" | ||
beforeEach(() => { | ||
server.use(http.get('/', resolver)) | ||
}) | ||
In this example, we call `server.use` in a `beforeEach` block with a `msw.rest.get` handler. This handler intercepts GET requests to the root URL and responds with the result of the `resolver` function. | ||
|
||
The `server.use` function adds the provided handlers to the current server instance for the duration of the current test. This means that the mock will only affect the test that follows the `beforeEach` block. After the test, the server is reset to its initial handlers. | ||
|
||
This approach is useful when you want to change the mock response for a specific test, or when you want to set up a mock that is only used in a single test. | ||
|
||
|
||
Running Tests | ||
------------- | ||
|
||
To run all tests, use the following command: | ||
|
||
.. code-block:: bash | ||
pnpm test:unit | ||
This will run all the Vitest tests in your project. | ||
|
||
Updating Snapshots | ||
------------------ | ||
|
||
If you make intentional changes to a component that affect its snapshot, you can update the snapshot with the following command: | ||
|
||
.. code-block:: bash | ||
pnpm test:unit -- -u | ||
This will update all snapshots in your project. |
Oops, something went wrong.