JavaScript Testing
🏞️

JavaScript Testing

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

 
When you have multiple tests checking whether 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.
When you have multiple tests checking whether 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.
 
If you change the file to which 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.
If you change the file to which 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 isolated jest --runInBand // To run tests sequentially
notion image
 
test.concurrent to indicate which ones Jest should execute concurrently with a test suite.
JavaScript
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.
 
JavaScript
module.exports = {
	testEnvironment: "node",
	globalSetup: "./globalSetup.js", // once before all tests  
	globalTeardown: "./globalTeardown.js" // once after all tests
};
 
JavaScript
// 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
JavaScript
expect.assertions(2);
expect.hasAssertions();
Expect the value could change
JavaScript
expect(result).toEqual({             
    cheesecake: 1,
    macarroon: 3,
    croissant: 3,
    eclaire: 7,
    generatedAt: expect.any(Date)}) //expect the generatedAt must be date
Spy a method
JavaScript
const logger = require("./logger");
beforeAll(() => jest.spyOn(logger, "logInfo"));
afterEach(() => logger.logInfo.mockClear());
Use mock to another achievement
JavaScript
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.
JavaScript
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

notion image

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

JavaScript
jest.spyOn(window.localStorage.___proto___,'getItem').mockReturnValue(JSON.stringfy());

Async testings

  1. pass done to the second callback function parameters.
JavaScript
// 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

JavaScript
const mockFn = Jest.fn();
mockFn.mockReturnValueOnce('Dell');
mockFn.mockReturnValue('Dell');
mockImplementation(()=>{});

Snapshot

JavaScript
// 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
JavaScript
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.
JavaScript
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
JavaScript
jest.setTimeout();
use FakeTimer
JavaScript
jest.useFakeTimers();
test('timer test',()=>{
	const fn = jest.fn();
	timer(fn);
	jest.runAllTimers(); // run all the callbacks of timers
	expect(fn).toHaveBeenCalledTimes(1);
});
 
JavaScript
jest.useFaketimers();
test('timer test',()=>{
	const fn = jest.fn();
	timer(fn);
	jest.runOnlyPendingTimers(); // run callbacks that are waiting
	expect(fn).toHaveBeenCalledTimes(1);
});
 
JavaScript
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 file
C#
beforeEach(() => {
  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
notion image
test(), it() is same.
JavaScript
it('suming 5 and 3 will return 7',()=>{
	const a:string = 5;
	expect(a).toBe(5);
	expect(sum(5,2)).toBe(7);
})
 
JavaScript
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
JavaScript
{
	"extends":"./tsconfig.json",
	"compilerOptions":{
		"jsx":"react-jsx"
	}
}
 
jest.config.js
JavaScript
module.exports = {
	preset: "ts-jest",
	testEnvironment: 'jsdom',
	globals:{
		'ts-jest':{
			tsconfig: './tsconfig.jest.json',
			},
		},
};
 
JavaScript
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
JavaScript
module.exports = {
	preset: "ts-jest",
	testEnvironment: 'jsdom',
	globals:{
		'ts-jest':{
			tsconfig: './tsconfig.jest.json',
			},
		},
	setupFilesAfterEnv:['./src/jest.setup.ts'],
};
 
jest.setup.ts
JavaScript
import '@testing-library/jest-dom';
 
.eslintrc.js
JavaScript
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
JavaScript
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'
	}
}
 
notion image
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)
JavaScript
//jest.config.js
module.exports = {
	setupFilesAfterEnv:
		['<rootDir>/setupJestDom.js'],
};

//setupJestDom.js
const jestDom = require("@testing-library/jest-dom");
expect.extend(jestDom);

SuperTest

JavaScript
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_schema
 

E2E

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.
notion image
 
Mocha which is exclusively a test runner
Chai which is exclusively an assertion library
 
These 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.
 
notion image
 
Puppeteer https://pptr.dev event-driven architecture. It control only Chrome and Chromium.

Cypress

Directly interfaces with a browser’s remote-control APIs
notion image
Cypress and Puppeteer, unlike Selenium, can directly control a browser instance.
 
Webpack configure and add environment
JavaScript
NODE_ENV=development npm run cypress:open
The execute order
notion image
 

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
JavaScript
const container = document.createElement('div');
container.setAttribute('id', 'root');
document.body.append(container);
render(<BrowserRouter><Header /></BrowserRouter>, { container });
when using links
JavaScript
render(<BrowserRouter><Gallery IMGurl={IMGurl} /></BrowserRouter>);
import used library
JavaScript
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.
 
JavaScript
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();
});
 
JavaScript
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.
TypeScript
const mockHistoryPush = jest.fn();
jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  useHistory: (): unknown => ({
    push: mockHistoryPush
  })
}));