Skip to content

Commit

Permalink
Merge pull request #176 from msqd/doc_frontend
Browse files Browse the repository at this point in the history
[READY] Doc frontend testing
  • Loading branch information
hartym authored Feb 23, 2024
2 parents ab8c221 + 26bda9f commit e8a2bc1
Show file tree
Hide file tree
Showing 6 changed files with 404 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/development/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Development
changelogs/index
roadmap
ideas
testing/index
124 changes: 124 additions & 0 deletions docs/development/testing/frontend/e2e_tests.rst
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.
71 changes: 71 additions & 0 deletions docs/development/testing/frontend/index.rst
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.
196 changes: 196 additions & 0 deletions docs/development/testing/frontend/unit_tests.rst
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.
Loading

0 comments on commit e8a2bc1

Please sign in to comment.