mirror of
https://github.com/wassname/Open-Assistant.git
synced 2026-07-16 01:10:06 +08:00
Merge branch 'unit-testing-jest-rtl'
This commit is contained in:
@@ -137,6 +137,17 @@ A few npm scripts are available for convenience:
|
||||
|
||||
Read more in the [./cypress README](cypress/).
|
||||
|
||||
## Unit testing
|
||||
|
||||
Jest and React Testing Library are used for unit testing JS/TS/TSX code.
|
||||
|
||||
- Store unit test files adjacent to the file being tested and have the filename
|
||||
end with `.test.ts` for non-React code or `.test.tsx` for React code.
|
||||
- `npm run jest`: automatically runs tests and watches for any relevant changes
|
||||
to rerun tests.
|
||||
|
||||
Read more in the [./src/README.md](src/README.md).
|
||||
|
||||
## Best Practices
|
||||
|
||||
When writing code for the website, we have a few best practices:
|
||||
|
||||
@@ -21,6 +21,7 @@ export default defineConfig({
|
||||
// implement node event listeners here
|
||||
getCompareSnapshotsPlugin(on, config);
|
||||
},
|
||||
specPattern: "cypress/components/*.cy.tsx",
|
||||
},
|
||||
|
||||
env: {
|
||||
|
||||
+5
-2
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
import { Container } from "./Container";
|
||||
import { Container } from "../../src/components/Container";
|
||||
|
||||
describe("<Container />", () => {
|
||||
it("renders", () => {
|
||||
@@ -8,6 +8,9 @@ describe("<Container />", () => {
|
||||
const className = "my-class";
|
||||
const text = "test_container";
|
||||
cy.mount(<Container className={className}>{text}</Container>);
|
||||
cy.get(`div.${className}`).should("have.class", className).should("be.visible").should("contain", text);
|
||||
cy.get(`div.${className}`)
|
||||
.should("have.class", className)
|
||||
.should("be.visible")
|
||||
.should("contain", text);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress", "node"],
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"esModuleInterop": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
|
||||
"include": ["**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
// jest.config.js
|
||||
const nextJest = require("next/jest");
|
||||
|
||||
const createJestConfig = nextJest({
|
||||
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
|
||||
dir: "./",
|
||||
});
|
||||
|
||||
// Add any custom config to be passed to Jest
|
||||
/** @type {import('jest').Config} */
|
||||
const customJestConfig = {
|
||||
// Add more setup options before each test is run
|
||||
setupFilesAfterEnv: ["<rootDir>/jest.setup.js"],
|
||||
// if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work
|
||||
moduleDirectories: ["node_modules", "<rootDir>/"],
|
||||
testEnvironment: "jest-environment-jsdom",
|
||||
};
|
||||
|
||||
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
|
||||
module.exports = createJestConfig(customJestConfig);
|
||||
@@ -0,0 +1,2 @@
|
||||
// jest.setup.js
|
||||
import "@testing-library/jest-dom/extend-expect";
|
||||
Generated
+10712
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,15 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"build-storybook": "build-storybook",
|
||||
"cypress": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"cypress:run:contract": "cypress run --config-file ./cypress.config.contract.js",
|
||||
"cypress:component": "cypress run --component",
|
||||
"cypress:image-baseline": "cypress-image-diff -u",
|
||||
"jest": "jest --watch",
|
||||
"fix:lint": "eslint --fix src/ --ext .js,.jsx,.ts,.tsx",
|
||||
"fix:format": "prettier --write ./src",
|
||||
"fix": "npm run fix:format && npm run fix:lint"
|
||||
@@ -72,6 +75,8 @@
|
||||
"@storybook/manager-webpack5": "^6.5.15",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@storybook/testing-library": "^0.0.13",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@types/node": "^18.11.17",
|
||||
"@types/react": "18.0.26",
|
||||
"@typescript-eslint/eslint-plugin": "^5.47.1",
|
||||
@@ -80,6 +85,8 @@
|
||||
"cypress-image-diff-js": "^1.23.0",
|
||||
"eslint-plugin-storybook": "^0.6.8",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^29.3.1",
|
||||
"jest-environment-jsdom": "^29.3.1",
|
||||
"prettier": "2.8.1",
|
||||
"prisma": "^4.7.1",
|
||||
"ts-node": "^10.9.1",
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Unit testing with Jest and React Testing Library
|
||||
|
||||
[Jest](https://jestjs.io/) is a test runner that is commonly coupled with
|
||||
[React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for creating unit tests.
|
||||
|
||||
## Creating tests
|
||||
|
||||
To begin writing tests, create a file ending in `.test.ts` for non-React code or a file ending with `.test.tsx` for
|
||||
testing React code. For example, a test file for React component `TaskOption.tsx` would be named `TaskOption.test.tsx`
|
||||
and would be created within the same directory as the component.
|
||||
|
||||
```
|
||||
// TaskOption.test.tsx
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { RouterContext } from "next/dist/shared/lib/router-context";
|
||||
import React from "react";
|
||||
import { createMockRouter } from "src/test-utils/createMockRouter";
|
||||
|
||||
import { OptionProps, TaskOption } from "./TaskOption";
|
||||
|
||||
describe("TaskOption component", () => {
|
||||
const testProps: OptionProps = {
|
||||
title: "Task Title",
|
||||
alt: "Task Title",
|
||||
img: "/imgPath",
|
||||
link: "/fake/path",
|
||||
};
|
||||
|
||||
it("should render", () => {
|
||||
render(<TaskOption {...testProps} />);
|
||||
expect(screen.getByRole("heading")).toHaveTextContent("Task Title");
|
||||
expect(screen.getByRole("img")).toHaveAttribute("alt", "Task Title");
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "/fake/path");
|
||||
});
|
||||
|
||||
it("should navigate properly on click", () => {
|
||||
const router = createMockRouter({ pathname: "/fake/path" });
|
||||
render(
|
||||
<RouterContext.Provider value={router}>
|
||||
<TaskOption {...testProps} />
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "/fake/path");
|
||||
fireEvent.click(screen.getByRole("link"));
|
||||
expect(router.push).toHaveBeenCalledWith("/fake/path", expect.anything(), expect.anything());
|
||||
});
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
This is a test that checks if the component has rendered correctly and if it navigates properly on click. The testing
|
||||
methods, component, and its props type are imported. The `describe` is a Jest method that is used to group related tests
|
||||
together while the `it` is what actually runs a test.
|
||||
|
||||
A testProps object is created and passed to the component to be rendered using the `render` method. Now it can be
|
||||
tested. Query methods like `getByRole` and others listed in the
|
||||
[testing library's docs](https://testing-library.com/docs/react-testing-library/cheatsheet#queries) can be used to
|
||||
search for elements in the page. Finally, `expect` is run on those elements to see if they match the values in the props
|
||||
that were originally declared. If they match, the test passes. If they do not match or if an error occurs, the test
|
||||
fails.
|
||||
|
||||
In the second test, a mock router is used to simulate Next's router. The TaskOption component is rendered with the
|
||||
router as its parent. To simulate a click, `fireEvent.click()` is used on the element with the role of "link". Finally,
|
||||
the router can be tested by verifying that the `router.push` method was called with the correct path. Since the push
|
||||
method is also called with two more arguments `as` and `options` that aren't useful for this specific test,
|
||||
`expect.anything()` is used to ensure that those arguments are not null or undefined.
|
||||
|
||||
## Running tests
|
||||
|
||||
```
|
||||
npm run jest
|
||||
```
|
||||
@@ -0,0 +1,34 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { RouterContext } from "next/dist/shared/lib/router-context";
|
||||
import React from "react";
|
||||
import { createMockRouter } from "src/test-utils/createMockRouter";
|
||||
|
||||
import { OptionProps, TaskOption } from "./TaskOption";
|
||||
|
||||
describe("TaskOption component", () => {
|
||||
const testProps: OptionProps = {
|
||||
title: "Task Title",
|
||||
alt: "Task Title",
|
||||
img: "/imgPath",
|
||||
link: "/fake/path",
|
||||
};
|
||||
|
||||
it("should render", () => {
|
||||
render(<TaskOption {...testProps} />);
|
||||
expect(screen.getByRole("heading")).toHaveTextContent("Task Title");
|
||||
expect(screen.getByRole("img")).toHaveAttribute("alt", "Task Title");
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "/fake/path");
|
||||
});
|
||||
|
||||
it("should navigate properly on click", () => {
|
||||
const router = createMockRouter({ pathname: "/fake/path" });
|
||||
render(
|
||||
<RouterContext.Provider value={router}>
|
||||
<TaskOption {...testProps} />
|
||||
</RouterContext.Provider>
|
||||
);
|
||||
expect(screen.getByRole("link")).toHaveAttribute("href", "/fake/path");
|
||||
fireEvent.click(screen.getByRole("link"));
|
||||
expect(router.push).toHaveBeenCalledWith("/fake/path", expect.anything(), expect.anything());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import AboutPage from "src/pages/about";
|
||||
|
||||
describe("About page", () => {
|
||||
it("should render correctly", () => {
|
||||
render(<AboutPage />);
|
||||
|
||||
expect(screen.getByRole("heading", { level: 1 })).toHaveTextContent("What is OpenAssistant?");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
import { NextRouter } from "next/router";
|
||||
|
||||
export function createMockRouter(router: Partial<NextRouter>): NextRouter {
|
||||
const mockRouter: NextRouter = {
|
||||
route: "/",
|
||||
pathname: "/",
|
||||
query: {},
|
||||
asPath: "/",
|
||||
basePath: "",
|
||||
defaultLocale: "en",
|
||||
domainLocales: [],
|
||||
isLocaleDomain: false,
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
back: jest.fn(),
|
||||
forward: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
beforePopState: jest.fn(),
|
||||
isFallback: false,
|
||||
isReady: true,
|
||||
isPreview: false,
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
...router,
|
||||
};
|
||||
return mockRouter;
|
||||
}
|
||||
@@ -17,5 +17,5 @@
|
||||
"baseUrl": "."
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": ["node_modules", "cypress"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user