Merge branch 'unit-testing-jest-rtl'

This commit is contained in:
ericv105
2023-01-10 19:14:40 -05:00
13 changed files with 10918 additions and 3 deletions
+11
View File
@@ -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:
+1
View File
@@ -21,6 +21,7 @@ export default defineConfig({
// implement node event listeners here
getCompareSnapshotsPlugin(on, config);
},
specPattern: "cypress/components/*.cy.tsx",
},
env: {
@@ -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);
});
});
+12
View File
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"types": ["cypress", "node"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"jsx": "react-jsx"
},
"include": ["**/*.ts", "**/*.tsx"]
}
+20
View File
@@ -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);
+2
View File
@@ -0,0 +1,2 @@
// jest.setup.js
import "@testing-library/jest-dom/extend-expect";
+10712
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -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",
+72
View File
@@ -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());
});
});
+10
View File
@@ -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;
}
+1 -1
View File
@@ -17,5 +17,5 @@
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
"exclude": ["node_modules", "cypress"]
}