Concepts
TDD
Test-Driven Development (TDD) involves writing test code before writing the code for a certain function. This helps ensure that the code passes the test and drives the entire development process. Benefits of TDD include reduced regression bugs, improved code quality, and increased testing coverage.
BDD
Behavior-Driven Development (BDD) enables project members, even those without coding experience, to use natural language to describe system functions and business logic. This allows for automated system testing based on these descriptions.
Example:
Scenario:
Adding a Page
Given I visit the editor page
And I see the canvas
And the canvas contains 1 page
When I click the add page button
Then the canvas contains 2 pages
Property-based testing
You can write a single test that automatically sends 1000 different inputs and checks for which input our code fails to return the correct response. You can use property-based testing with your preferred test runner (e.g. Mocha, Jest, etc.) by using libraries such as js-verify, fast-check, or testcheck (with better documentation).
Framework
JEST

loggingService
writes to the correct file, all of them will fail if you modify loggingService
. The more tests you have, the higher the cost of changing loggingService
becomes.
loggingService
writes, its tests will fail. The addItemToCart
tests will still pass, as they are doing what you expect: using the logging service. Structuring your tests this way gives you fewer tests to update and more precise feedback about which part of your software doesn't meet the tests' requirements.JEST run test across multi files
Make sure that tests are well isolatedjest --runInBand // To run tests sequentially

test.concurrent
to indicate which ones Jest should execute concurrently with a test suite.describe("addItemToCart", () => { test.concurrent("add an available item to cart", async () => { /* */ }); test.concurrent("add unavailable item to cart", async () => { /* */ }); test.concurrent("add multiple items to cart", async () => { /* */ }); });
To control the number of tests running simultaneously, use the
-maxConcurrencyOption
option. To manage the number of worker threads spawned for running tests, use the -maxWorkers
option and specify the desired number of threads.Global hooks
Jest provides two configuration options,
globalSetup
and globalTeardown
, to set up global hooks. These options can be specified in the jest.config.js
file.module.exports = { testEnvironment: "node", globalSetup: "./globalSetup.js", // once before all tests globalTeardown: "./globalTeardown.js" // once after all tests };
// globalSetup.js const setup = async () => { global._databaseInstance = await databaseProcess.start() }; module.exports = setup; /*Values assigned to the global object, like the one shown previously, will be available on the globalTeardown hook, too.*/ // globalTeardown.js const teardown = async () => { await global._databaseInstance.stop() }; module.exports = teardown;
Assertion
Ensure the expect to run
expect.assertions(2); expect.hasAssertions();
Expect the value could change
expect(result).toEqual({ cheesecake: 1, macarroon: 3, croissant: 3, eclaire: 7, generatedAt: expect.any(Date)}) //expect the generatedAt must be date
Spy a method
const logger = require("./logger"); beforeAll(() => jest.spyOn(logger, "logInfo")); afterEach(() => logger.logInfo.mockClear());
Use mock to another achievement
jest.spyOn(logger, "logInfo").mockImplementation(jest.fn());
In Jest, all stubs are spies, but not all spies are stubs.
Spies
record data related to the usage of a function without interfering in its implementation.
Stubs
record data associated with the usage of a function and change its behavior, either by providing an alternative implementation or return value.
Mocks
change a function’s behavior, but instead of just recording information about its usage, they have expectations preprogrammed.
Mock behaviour
mockClear
erases a test double’s records but keeps the double in place.
mockReset
erases a test double’s records and any canned behavior but keeps the double in place.
mockRestore
completely removes the double, restoring the original implementation.test("mock random and restore", () => { jest.spyOn(Math, "random").mockReturnValue(0.5); const number = getRandomNumber(); expect(number).toBe(1); // 0.5 + 0.5 jest.spyOn(Math, "random").mockRestore(); });
How spyOn work

when to choose mock
or spyOn
- If you are mocking an object’s property, you should probably use
jest.spyOn
.
- If you are mocking an import, you should probably use
jest.mock
.
- In case you have to use the same replacement in multiple test files, you should, ideally, use a manual mock placed on the
mocks
folder.
React Testing Library is not a test runner
local Storage
jest.spyOn(window.localStorage.___proto___,'getItem').mockReturnValue(JSON.stringfy());
Async testings
- pass done to the second callback function parameters.
// method 1 when pass but with done test('test async', (done)=>{ fetchData(data=>{ expect(data).toEqual({a:1}); done(); }) }) // method 2 return promise test('test async', ()=>{ return fetch('http://aa.com').then(data=>{ expect(data).toEqual({a:1}); }) })
Mock
const mockFn = Jest.fn(); mockFn.mockReturnValueOnce('Dell'); mockFn.mockReturnValue('Dell'); mockImplementation(()=>{});
Snapshot
// if a arrtibute is always change expect(config()).toMatchSnapshot({ time: expect.any(Date); }) toMatchcInlineSnapshot // generate string in the test files toMatchSnapshot // generate string in a seperate file
Mock a function but preserve others from a module
const mockInvalidateQueries = jest.fn(); jest.mock('react-query', () => ({ ...jest.requireActual('react-query'), useQueryClient: () => { return { invalidateQueries: mockInvalidateQueries }; } }));
Returns the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not.
jest.mock('../myModule', () => { // Require the original module to not be mocked... const originalModule = jest.requireActual('../myModule'); return { __esModule: true, // Use it when dealing with esModules ...originalModule, getRandom: jest.fn().mockReturnValue(10), }; }); const getRandom = require('../myModule').getRandom; getRandom(); // Always returns 10
Settimeout to test
jest.setTimeout();
use
FakeTimer
jest.useFakeTimers(); test('timer test',()=>{ const fn = jest.fn(); timer(fn); jest.runAllTimers(); // run all the callbacks of timers expect(fn).toHaveBeenCalledTimes(1); });
jest.useFaketimers(); test('timer test',()=>{ const fn = jest.fn(); timer(fn); jest.runOnlyPendingTimers(); // run callbacks that are waiting expect(fn).toHaveBeenCalledTimes(1); });
jest.useFaketimers(); test('timer test',()=>{ const fn = jest.fn(); timer(fn); jest.advanceTimersByTime(3000); // advance time expect(fn).toHaveBeenCalledTimes(1); });
Difference between jest.doMock
and jest.mock
jest.doMock
need special set up steps. When using
babel-jest
, calls to mock
will automatically be hoisted to the top of the code block. Use this method if you want to explicitly avoid this behaviour. One example when this is useful is when you want to mock a module differently within the same filebeforeEach(() => { jest.resetModules(); }); test('moduleName 1', () => { // The optional type argument provides typings for the module factory jest.doMock<typeof import('../moduleName')>('../moduleName', () => { return jest.fn(() => 1); }); const moduleName = require('../moduleName'); expect(moduleName()).toBe(1); }); test('moduleName 2', () => { jest.doMock<typeof import('../moduleName')>('../moduleName', () => { return jest.fn(() => 2); }); const moduleName = require('../moduleName'); expect(moduleName()).toBe(2); });
Unit test, integration test, UI test

test()
, it()
is same. it('suming 5 and 3 will return 7',()=>{ const a:string = 5; expect(a).toBe(5); expect(sum(5,2)).toBe(7); })
import {render} from '@testing-library/react'; test('renders "hello world"',()=>{ render(<Hello/>); }
Will trow error and we need to set a new ts config file
tsconfig.jest.json
{ "extends":"./tsconfig.json", "compilerOptions":{ "jsx":"react-jsx" } }
jest.config.js
module.exports = { preset: "ts-jest", testEnvironment: 'jsdom', globals:{ 'ts-jest':{ tsconfig: './tsconfig.jest.json', }, }, };
import {render, screen} from '@testing-library/react'; test('renders "hello world"',()=>{ render(<Hello/>); const myElement = screen.getByText(/Hello World/); expect(myElement).toBeInTheDocument(); }
jest.config.js
module.exports = { preset: "ts-jest", testEnvironment: 'jsdom', globals:{ 'ts-jest':{ tsconfig: './tsconfig.jest.json', }, }, setupFilesAfterEnv:['./src/jest.setup.ts'], };
jest.setup.ts
import '@testing-library/jest-dom';
.eslintrc.js
extends:[ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommend-requireing-type-checking' //new add 'next', 'next/core-web-vitals' ], 'parserOptions':{ 'project':'./tsconfig.json', 'ecmaFeatures':{ 'jsx':true } } 'rules':{ '@typescript-eslint/explicit-module-boundary-types':'off' } }
Provide eslintrc suggestions on Jest test library
.eslintrc.js
extends:[ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommend-requireing-type-checking', 'plugin:jest/recommended', //new add 'plugin:jest/style', //new add 'next', 'next/core-web-vitals' ], 'parserOptions':{ 'project':'./tsconfig.json', 'ecmaFeatures':{ 'jsx':true } } 'rules':{ '@typescript-eslint/explicit-module-boundary-types':'off' } }

We're not running our tests in a browser; we're running them in the terminal with Node.js. Node.js does not have the DOM API that is typically included with browsers. To simulate a browser environment in Node.js, Jest uses an npm package called jsdom, which is essential for testing React components.
Using Code Coverage
npm test -- --coverage
generate the /coverage/lcov-report/index.html to give interactive information.@testing-library/dom
provides screen
, getByText
@testing-library/jest-dom
to use it we need to add (provides toBeInTheDocument
)//jest.config.js module.exports = { setupFilesAfterEnv: ['<rootDir>/setupJestDom.js'], }; //setupJestDom.js const jestDom = require("@testing-library/jest-dom"); expect.extend(jestDom);
SuperTest
const request = require('supertest'); //app is the url or the listenner object test("",()=>{ const response = await request(app) .post('/carts/add') .set("authorization", authHeader) .send({item: "cheesecake", quantity:3}) .expect(200) .expect("Content-Type",/json/); // response.body })
testing with databases and third-party APIs.
knexjs
⇒ ./node_modules/.bin/knex migrate:make —env development initial_schemaE2E
Selenium
To communicate with the Webdriver, Selenium uses a protocol called JSON Wire. This protocol specifies a set of HTTP routes for handling different actions to be performed within a browser.
JSON Wire
To communicate with the browser, each Webdriver uses the target browser’s remote-control APIs. Because different browsers have distinct remote-control APIs, each browser demands a specific driver.

Mocha
which is exclusively a test runnerChai
which is exclusively an assertion libraryNightwatch.js
https://nightwatchjs.orgWebdriverIO
https://webdriver.ioplaywright
https://playwright.devThese tools, just like Selenium, can interface with multiple Webdrivers and, therefore, are capable of controlling real browsers. The main difference between these libraries and Selenium is that they ship with testing utilities.

Puppeteer
https://pptr.dev event-driven architecture. It control only Chrome and Chromium.Cypress
Directly interfaces with a browser’s remote-control APIs

Cypress
and Puppeteer
, unlike Selenium, can directly control a browser instance. Webpack configure and add environment
NODE_ENV=development npm run cypress:open
The execute order

CodeceptJS
CodeceptJS
is an end-to-end testing framework for web applications written in JavaScript. It offers a user-friendly interface for writing and running tests. CodeceptJS
is compatible with popular front-end frameworks such as AngularJS
, ReactJS
, and VueJS
, and can execute tests on different browsers, web drivers, or headless browsers. Additionally, it integrates with various test runners like Mocha
, Jest
, and Protractor
, and can be used with cloud services like BrowserStack
and SauceLabs
for cross-browser testing.Experience
When testing a component which has use createProtol
const container = document.createElement('div'); container.setAttribute('id', 'root'); document.body.append(container); render(<BrowserRouter><Header /></BrowserRouter>, { container });
when using links
render(<BrowserRouter><Gallery IMGurl={IMGurl} /></BrowserRouter>);
import used library
import { render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import userEvent, { TargetElement } from '@testing-library/user-event';
To perform a snapshot test, render the component and get back the returned value of
asFragment
. Ensure that the fragment matches the snapshot. The result of the snapshot test is a test.js.snap
file in the snapshot
folder, which is created automatically in the same directory as the test.js
file when the tests are run.it("snapshot header component", () => { const mockText = "This is just for the sake of the test"; const { asFragment } = render(<Header text={mockText} />); expect(asFragment(<Header text={mockText} />)).toMatchSnapshot(); });
expect(getByText(mockText)).not.toBeNull(); expect(getByText(mockText)).toBeInTheDocument(); //same as above expect(screen.getByRole('heading')).toHaveTextContent('hello there')
Mock a module
Should put out side of descriptions.
const mockHistoryPush = jest.fn(); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), useHistory: (): unknown => ({ push: mockHistoryPush }) }));