Testing Javascript
Main Job of
Testing Frameworks
- make error beautiful
- make errors useful
- identify where the bug occured
- proper line number
= To quickly identify bug
Expect
function expect(actual){
return {
toBe(expected){
if(result!==expcted){
throw new Error(`${actual} is not equal to ${expected}`)
}
},
toEqual(expected){},
toBeGreaterThan(expected){}
}
}
- simpler than multiple if statements
- chainable
const result = sum(4,5)
expected = 9
expect(result).toBe(expected)
const result = sum(4,5)
expected = 9
if(result!==expected){
throw new Error(`${result} is not equal to ${expected}`)
}
test
async function test(title, callback){
try{
await callback() // async because callback can be a promise
console.log(`✅ ${title}`)
}catch(err){
console.error(`❌ ${title}`)
console.error(err)
}
}
- run multiple tests
- better error message
test('sum adds two numbers',()=>{
const result = sum(2,4)
const expected = 7
expect(result).toBe(expected)
})
test('sum adds two numbers',()=>{
const result = sum(2,4)
const expected = 6
expect(result).toBe(expected)
})
const result = sum(2,4)
expected = 7
expect(result).toBe(expected)
// --- stops here because of error ---
const result = sum(2,4)
expected = 6
expect(result).toBe(expected)
Mocking
monkey patching
const thumbwar = require('./thumbwar')
const utils = require('./utils')
test('returns winner',()=>{
const originalGetWinner = utils.getWinnner
utils.getWinner = (p1,p2) => p1 //<--- main patching
const winner = thumbwar('A','B')
expect(winner).toBe('A')
//cleanup
utils.getWinner = originalGetWinner
})
to avoid implementation detail
jest.fn
const thumbwar = require('./thumbwar')
const utils = require('./utils')
test('returns winner',()=>{
const originalGetWinner = utils.getWinnner
utils.getWinner = jest.fn((p1,p2) => p1)
const winner = thumbwar('A','B')
expect(winner).toBe('A')
expect(utils.getWinner).toHaveBeenCalledTimes(2)
expect(utils.getWinner).toHaveBeenCalledWith('A','B')
expect(utils.getWinner.mock.calls).toEqual([
['A','B']
])
//cleanup
utils.getWinner = originalGetWinner
})
- keeps track of what argument it is called with, etc
function fn(impl){
const mockFn = (...args){
mockFn.mock.calls.push(args)
impl(...args)
}
mockFn.mock = {calls:[]}
return mockFn
}
jest.spyOn
test('returns winner',()=>{
const originalGetWinner = utils.getWinnner
utils.getWinner = jest.fn((p1,p2) => p1)
//expect()
//cleanup
utils.getWinner = originalGetWinner
})
- keeping track of original function is annoying
function fn(impl=()=>{}){
const mockFn = (...args){
mockFn.mock.calls.push(args)
impl(...args)
}
mockFn.mock = {calls:[]}
mockFn.mockImplementation = newImpl => (impl = newImpl)
return mockFn
}
function spyOn(obj, prop){
const originalValue = obj
obj[prop]= fn()
obj[prop].mockRestore = ()=>{obj[prop] = originalValue}
}
test('returns winner',()=>{
jest.spyOn(utils,'getWinner')
utils.getWinner.mockImplementation((p1,p2) => p1))
//expect
//cleanup
utils.getWinner.mockRestore()
})
Beware that mockFn.mockRestore only works when the mock was created with jest.spyOn. Thus you have to take care of restoration yourself when manually assigning jest.fn()
jest.mock
jest.mock('..utils/',()=>{
return {
getWinner: jest.fn((p1,p2)=>p1)
}
})
test('returns winner',()=>{
//expect()
//cleanup
utils.getWinner.mockReset()
})
- to mock a module
const utilsPath = require.resolve('../utils')
require.cache[utilsPath] = {
id:utilsPath,
filename: utilsPath,
loaded:true,
exports:{
getWinner: fn((p1,p2)=>p1)
}
}
// clean up
delete require.cache[utilsPath]
test('returns winner',()=>{
jest.spyOn(utils,'getWinner')
utils.getWinner.mockImplementation((p1,p2) => p1))
//expect
//cleanup
utils.getWinner.mockReset()
})
__mocks__
jest.mock('..utils/',()=>{
return {
getWinner: jest.fn((p1,p2)=>p1)
}
})
test('returns winner',()=>{
//expect()
//cleanup
utils.getWinner.mockReset()
})
- global mocking
- no need to provide custom implementation again & again
//__mocks__/utils.js
module.exports = {
getWinner: jest.fn((p1,p2)=>p1)
}
jest.mock('..utils/')
test('returns winner',()=>{
//expect()
//cleanup
utils.getWinner.mockReset()
})
Tearing Down
type | action |
---|---|
mockFn.mockClear() jest.clearAllMocks() |
clears calls, instances, results |
mockFn.mockReset() jest.resetAllMocks() |
mockClear() + clear return values & implementation |
mockFn.mockRestore() jest.restoreAllMocks() |
mockReset() + restore to original |
Testing Backend
1. Pure Function:
describe('isPassWordAcceptable',()=>{
test('valid password',()=>{
expect(isPasswordAcceptable('Aabc!')).toBeTruthy()
})
test('too short',()=>{
expect(isPasswordAcceptable('Aab!')).toBeFalsy()
})
test('no uppercase',()=>{
expect(isPasswordAcceptable('aabc!')).toBeFalsy()
})
test('no lowercase',()=>{
expect(isPasswordAcceptable('AABC!')).toBeFalsy()
})
})
describe('isPassWordAcceptable',()=>{
allowedPasswords = ['Aabc!']
disallowedPasswords = ['Aab!','aabc!','AABC!']
allowedPasswords.forEach(password=>{
test('valid password',()=>{
expect(isPasswordAcceptable(password)).toBeTruthy()
})
})
disallowedPasswords.forEach(password=>{
test('valid password',()=>{
expect(isPasswordAcceptable(password)).toBeFalsy()
})
})
})
✅ 1st idea -> use loop
❌ Lot of Duplicate code
ways to handle multiple conditions
const disallowedPasswords = [
{
reason: "too short",
value: "123",
},
{
reason: "too big",
value: "1234567",
},
];
const allowedPassowords = ["1234", "123456", "12345"];
test.each(disallowedPasswords)("$reason", ({ password }) => {
expect(isPasswordAcceptable(password)).toBeFalsy();
});
test.each(allowedPassowords)("valid", (password) => {
expect(isPasswordAcceptable(password)).toBeTruthy();
});
✅ 2nd idea -> use jest's inbuilt test.each
❌ But since reasons not present, error msgs are not helpful
describe('isPassWordAcceptable',()=>{
allowedPasswords = ['Aabc!']
disallowedPasswords = ['Aab!','aabc!','AABC!']
disallowedPasswords.forEach(password=>{
test('valid password',()=>{
expect(isPasswordAcceptable(password)).toBeFalsy()
})
})
})
2. Middlewares
2. Generating data for multiple branch
const getMyTestObj = (overrides) => ({
error: jest.fn(),
req: jest.fn(),
res: jest.fn(),
next: jest.fn(),
...overrides,
})
describe('error-middleware', () => {
test('headerssent', () => {
const {error, req, res, next} = getMyTestObj({res: {headersSent: true}})
errorMiddleware(error, req, res, next)
expect(next).toHaveBeenCalledWith(error)
...
})
test('error instanceof UnauthorizedError', () => {
const {error, req, res, next} = getMyTestObj({
error: new UnauthorizedError('401', {message: 'Not authorized'}),
res: {headersSent: false, status: jest.fn(), json: jest.fn()},
})
errorMiddleware(error, req, res, next)
expect(res.status).toHaveBeenCalledWith(401)
...
})
})
✅ use factory function
❌ difficult to know what changed b/w test
describe('error-middleware', () => {
test('headerssent', () => {
const error = jest.fn()
const req = jest.fn()
const res = {headersSent: true}
const next = jest.fn()
errorMiddleware(error, req, res, next)
expect(next).toHaveBeenCalledWith(error)
...
})
test('error instanceof UnauthorizedError', () => {
const error = new UnauthorizedError('401', {message: 'Not authorized'})
const req = jest.fn()
const res = {headersSent: false, status: jest.fn(), json: jest.fn()}
const next = jest.fn()
errorMiddleware(error, req, res, next)
expect(res.status).toHaveBeenCalledWith(401)
...
})
})
3. Controllers
- often interact with database & other services
- collection of middleware that applies some business logic
Snapshot Testing
- Big snapshots are difficult to maintain; Take small, focused snapshots
References
- What all is available in global scope: (eq: describe, test):
- https://jestjs.io/docs/api
- Assertions (expect().toBe()):
- https://jestjs.io/docs/expect
Service Worker
- Offline jobs
- Notification
- Caching
- intercept n/w request
- prefetch stuff
Web Worker
- Extra pair of hands
- Computation
Browser APIs
Both run worker context.
Therefore non-blocking
Service Worker
- proxy that sits b/w browser & network
Web Worker
- scripts that run in background
// main thread
var myWorker = new Worker('multiplyWorker.js');
myWorker.postMessage([first.value, second.value])
// multiplyWorker.js
onmessage = function(e) {
console.log('Message received from main script');
var workerResult = (e.data[0] * e.data[1]);
console.log('Posting message back to main script');
postMessage(workerResult);
}
// main thread
myWorker.terminate();
Testing Javascript
By xerosanyam
Testing Javascript
- 858