[TDD] 단위 테스트와 TDD(테스트 주도 개발) 프로그래밍 방법 소개 - (1/5)
개발이 끝난 뒤에 문제가 없는지 확인하기 위해 애플리케이션을 실행하고, 직접 수동 (통합) 테스트를 진행해야 한다. 단위 테스트를 작성하지 않은 코드들은 테스트를 작성하지 않은 코드들 보다 버그가 잠재되어 있을 확률이 높은데, 문제는 직접 테스트 하는 비용이 너무 크다는 것이다. 그 이유는 통합 테스트를 위해서는 캐시, 데이터베이스 등 외부 컴포넌트들과 연결 등 부가적인 시간이 필요하기 때문이다.
테스트 코드를 작성하지 않았다면 여러 개의 버그가 잠재되어 있을 확률이 있고, 모든 버그들을 수정하고 테스트를 반복하는 비용은 기하급수적으로 늘어나게 된다. 그러므로 우리는 개발 및 테스팅에 대한 비용을 줄이기 위해 단위 테스트를 작성해야 한다.
npm instal -D jest
: 테스팅 툴은 개발 시에만 사용하기 때문에 -D 옵션으로 설치npm test
, npm test testfilename
npm test example.test.js
{
"name": "fsmarket-backend",
"version": "0.0.0",
"type": "module",
"scripts": {
"start": "pm2 start ecosystem.config.json",
"test": "jest",
"dev": "PORT=5001 nodemon server.js"
},
...
}
npm test
로 테스트 코드를 실행할 수 있으며 test 가 들어 모든 코드를 찾아서 실행[JEST] 모킹 Mocking 정리 (jest.fn / jest.mock /jest.spyOn)
[Jest]jest.fn(), jest.spyOn() 함수 모킹
test('did not rain', () => {
expect(inchesOfRain()).toBe(0);
});
참고: 테스트에서 promise가 반환 되면 Jest는 promise가 해결될 때까지 대기한 다음에 테스트를 완료합니다.
Jest는 테스트 함수에 done
이라고 callback 함수 인수를 제공하면 대기합니다.
예를 들어, fetchBeverageList()
lemon 이 포함된 목록으로 확인되어야 하는 약속을 반환한다고 가정해 보겠습니다.
test('has lemon in it', () => {
return fetchBeverageList().then(list => {
expect(list).toContain('lemon');
});
});
test에 대한 호출 이 즉시 반환 되더라도 Promise가 해결될 때까지 테스트 완료를 보류
expect(value)
: value 는 실제 코드.toBe(value)
: 기본 값을 비교하거나 개체 인스턴스의 참조 ID를 확인하는 데 사용합니다..toEqual(value)
: 객체 인스턴스의 모든 속성을 재귀적으로 비교하는 데 사용===
엄격한 동등 연산자 보다 테스트에 더 나은 Object.is
원시 값을 비교하도록 호출 합니다.const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
describe('the La Croix cans on my desk', () => {
test('have all the same properties', () => {
expect(can1).toEqual(can2);
});
test('are not the exact same can', () => {
expect(can1).not.toBe(can2);
});
});
const binaryStringToNumber = binString => {
if (!/^[01]+$/.test(binString)) {
throw new CustomError('Not a binary number.');
}
return parseInt(binString, 2);
};
describe('binaryStringToNumber', () => {
describe('given an invalid binary string', () => {
test('composed of non-numbers throws CustomError', () => {
expect(() => binaryStringToNumber('abc')).toThrowError(CustomError);
});
test('with extra whitespace throws CustomError', () => {
expect(() => binaryStringToNumber(' 100')).toThrowError(CustomError);
});
});
describe('given a valid binary string', () => {
test('returns the correct number', () => {
expect(binaryStringToNumber('100')).toBe(4);
});
});
});
test
필수 사항은 아님.describe
중첩 가능test()
에 전달된 함수 앞에 async 키워드를 사용test('the data is peanut butter', async () => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
test('조회가 되는 경우', async () => {
// given
const address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A300b00';
const blockchainId = 1;
// when
const wallet = await walletService.findByAddressAndBlockchainId(address, blockchainId);
// then
expect(wallet.address).toEqual('0xFDEa65C8e26263F6d9A1B5de9555D2931A300b00');
expect(wallet.blockchainId).toEqual(1);
}
not.toBe(...)
테스트에서는 undefined, null, false를 구별할 필요가 있는 경우 사용
toBeNull
: null인지만 확인toBeUndefiend
toBeDefined
: toBeUndefiend 의 반대toBeTruthy
: if 문이 참인가toBeFalsy
: if 문이 거짓인가숫자 비교
toBeGreaterThan
: ~보다 큼toBeGreaterThanOrEqual
: ~보다 크거나 같음toBeLessThan
: ~보다 작음toBeLessThanOrEqual
: ~보다 작거나 같음test('two plus two', () => {
const value = 2 + 2;
expect(value).toBeGreaterThan(3);
expect(value).toBeGreaterThanOrEqual(3.5);
expect(value).toBeLessThan(5);
expect(value).toBeLessThanOrEqual(4.5);
// toBe and toEqual are equivalent for numbers
expect(value).toBe(4);
expect(value).toEqual(4);
});
소수를 다룰 때는 toEqual
는 반올림을 하기 때문에 대신 toBeCloseTo
를 사용
test('adding floating point numbers', () => {
const value = 0.1 + 0.2;
//expect(value).toBe(0.3); This won't work because of rounding error
expect(value).toBeCloseTo(0.3); // This works.
});
toMatch
: 문자열에 대한 정규식표현을 확인test('there is no I in team', () => {
expect('team').not.toMatch(/I/);
});
test('but there is a "stop" in Christoph', () => {
expect('Christoph').toMatch(/stop/);
});
toContain
: 배열이나 iterable 에 값이 있는지 확인 할때.const shoppingList = [
'diapers',
'kleenex',
'trash bags',
'paper towels',
'milk',
];
test('the shopping list has milk on it', () => {
expect(shoppingList).toContain('milk');
expect(new Set(shoppingList)).toContain('milk');
});
toThrow
: 특정 함수가 호출될 때 오류가 발생하는지 여부를 테스트function compileAndroidCode() {
throw new Error('you are using the wrong JDK');
}
test('compiling android goes as expected', () => {
expect(() => compileAndroidCode()).toThrow();
expect(() => compileAndroidCode()).toThrow(Error);
// You can also use the exact error message or a regexp
expect(() => compileAndroidCode()).toThrow('you are using the wrong JDK');
expect(() => compileAndroidCode()).toThrow(/JDK/);
});
afterAll(fn, timeout)
beforeAll(fn, timeout)
jest.js - 모의 함수
[JEST] 📚 모킹 Mocking 정리 (jest.fn / jest.mock /jest.spyOn)
[번역] Jest Mocks에 대한 이해
실제 데이터 베이스 접근과 외부 API 연동을 해야하는 부분은 직접 가져다가 붙이는 경우 DB 설치 / 데이터 존재 / DB Connection 등등에서 발생하는 문제가 해당 기능의 문제인지 DB의 문제인지도 알기 어려우며, 오작동 / 비용이 발생하는 외부 API 연동시 테스트를 진행하기 어렵고 실질적인 기능 단위의 테스트를 수행하기 어렵다.
따라서 DB 나 외부 API를 통한 데이터를 정상적으로 받아온다고 가정하고 하고 테스트 해야한다.
jest.fn
: Mock a functionjest.mock
: Mock a modulejest.spyOn
: Spy or mock a functionconst mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();
// With a mock implementation:
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue()); // true;
Mocking 한 것에 대한 내용
jest.fn() 해서 Mock 함수를 생성했을 때 담기는 것: [Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
mock property 에는 calls 라 불리는 배열이 존재하는데
mock 함수가 불릴 때마다의 인수 정보 등이 담겨 있다.
{
calls: [ [], [ 1 ] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results: [
{ type: 'return', value: undefined },
{ type: 'return', value: 2 } // mock.mockReturnValue[Once](2); 처럼 값을 넣는다면..
],
lastCall: [ 1 ]
}
jest.mock('../app/controllers/likes.controller.js'); // 해당 대상을 Mocking 화
import likesController from '../app/controllers/likes.controller.js';
jest.mock('../app/service/likes.service.js');
import likesService from '../app/service/likes.service.js';
jest.mock('../app/database/connection.js');
import Connection from '../app/database/connection.js';
jest.mock('../app/database/query.js');
import Query from '../app/database/query.js';
//describe , test 할 것들을 묶는 큰 목록
describe('Mockiong 정리', () => {
describe('likesController middleware Test', () => {
const res = {
send: jest.fn(),
status: jest.fn(() => res),
json: jest.fn(),
};
// it/test(테스트 이름: String, fn) 단위 테스트 케이스
test('좋아요를 눌렀을 때 필드 값이 비어있는 경우', async() => {
const req = {
errors: [] // id, params 이 없어서 validation 단계에서 에러 났다고 가정
};
await likesController.createLikes(req, res);
res.status.mockReturnValue(400);
expect(res.status()).toBe(400);
});
test('좋아요를 눌렀을 때 정상적으로 추가 되었을 경우', async() => {
const req = {
id: 1,
params: { id: 1 },
};
// 정상적인 케이스로 실행 시 return 될 값을 임의로 작성
const result = {
success: true,
response: 'insert된 결과 값',
// error는 null 이라 중요한 것이 아니라서 체크하지 않음.
}
res.json.mockReturnValue(result);
await likesController.createLikes(req, res);
expect(res.json().success).toBeTruthy(); // 값이 참인지 확인
expect(res.json().response).toMatch(/insert된 결과 값/);
});
test('좋아요를 눌렀을 때 좋아요 실패 했을 경우', async() => {
const req = {
id: 1,
params: { id: 1 },
};
// 정상적인 케이스로 실행 시 return 될 값을 임의로 작성
const result = {
success: false,
// response는 null 이라 중요한 것이 아니라서 체크하지 않음.
error: {
status: 500,
message: '[LikesService] Error create likes',
},
}
res.status.mockReturnValue(500);
res.json.mockReturnValue(result);
await likesController.createLikes(req, res);
expect(res.status()).toBe(500); // 주소 및 값이 같은 객체인지 확인.
expect(res.json().success).toBeFalsy(); // Falsy -> false 인지 확인.
expect(res.json().error.message).toMatch(/LikesService/); // toMatch(/Match 되야하는 문자열/)
});
});
describe('likesService Func Test', () => {
test('findLikes() 호출', async() => {
const nftId = 1;
const walletId = 1;
const result = {
nftId: nftId,
walletId: walletId,
};
likesService.findLikes.mockReturnValue(result); // 원하는 반환 값을 설정.
const findLikes = await likesService.findLikes(nftId, walletId); // 필요한 값을 넣었다고 가정.
expect(findLikes).toHaveProperty('nftId', 1);
expect(findLikes).toHaveProperty('walletId', 1);
});
// 보류
test.skip('findLikes() 에러', async() => {
const nftId = 1;
const walletId = 1;
likesService.findLikes.mockReturnValue(() => {throw new Error('[LikesService] Error finding likes')}); // 원하는 반환 값을 설정.
console.log(likesService.findLikes(nftId, walletId));
expect( () => {
const result = likesService.findLikes(nftId, walletId);
}).toThrowError(/[LikesService] Error finding likes/);
});
test('createLikes() 호출', async() => {
const nftId = 566;
const walletId = 215;
const result = {
nftId: nftId,
walletId: walletId,
};
likesService.createLikes.mockReturnValue(result); // 원하는 반환 값을 설정.
const findLikes = await likesService.createLikes(nftId, walletId); // 필요한 값을 넣었다고 가정.
expect(findLikes).toHaveProperty('nftId', 566);
expect(findLikes).toHaveProperty('walletId', 215);
});
});
describe('likes Query Test', () => {
let conn;
beforeAll( async() => {
conn = await Connection.getKnex();
});
afterAll( async()=> {
await Connection.closeAllConnections();
});
test('selectLikes() 실행', async () => {
const params = {
nftId: 566,
walletId: 215,
}
Query.selectOneLikes.mockReturnValue(params);
const likeResult = await Query.selectOneLikes(conn, params);
expect(likeResult).toHaveProperty('nftId', 566);
expect(likeResult).toHaveProperty('walletId', 215);
});
test('insertLikes() 실행', async () => {
const params = {
nftId: 566,
walletId: 215,
}
Query.insertLikes.mockReturnValue(1);
const likeResult = await Query.insertLikes(conn, params);
expect(likeResult).toBe(1);
});
});
});
ECMAScript Modules - jest docs
jest.mock은 Babel #10025 없이 ES 모듈을 조롱하지 않습니다 .
How to use ESM tests with jest
Jest import (ESM)기능 활성화하기 (with 프로그래머스 과제관)
export default async () => {
return {
transform: {},
};
};
node --experimental-vm-modules node_modules/jest/bin/jest.js
추가"scripts": {
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
},
import { jest, beforeAll, describe, test, expect } from '@jest/globals';
jest.mock('../utils/connection.js');
let Connection;
beforeAll( async () => {
({ default: Connection } = await import('../utils/connection.js') );
})
describe('test connection.js', () => {
test('knex connect test', async () => {
const mockGetKnex = jest.spyOn(Connection, 'getKnex');
const expectResult = "Query execution";
mockGetKnex.mockResolvedValue(expectResult);
const conn = await Connection.getKnex();
console.log('invocationCallOrder: ', mockGetKnex.mock.invocationCallOrder);
expect(mockGetKnex).toHaveBeenCalled();
expect(conn).toBe(expectResult);
mockGetKnex.mockRestore();
});
})
jest.spyOn(object, methodName) : 원래 함수를 덮어쓰고 함수를 감시하는 spy 함수를 만들어서 적용한다.
import {jest} from '@jest/globals';
를 이용할 때 Mock Function 확인
-> async / await 로 감싸야한다.
test.only("should not pass", async (done) => {
try {
const a = await getAsync()
expect(a).toEqual(2)
done()
} catch (e) {
// have to manually handle the failed test with "done.fail"
done.fail(e)
}
})
describe
에서는 Promise 지원하지 않는다 했음.test()
에 함수를 넘겨 줄때 Promise 형식으로 넘겨 주면 되었음.--detectOpenHandles
은 디버깅 시 사용하며 그 성능이 많이 저하된다고 하기 때문에 사용하지 않는다고 합니다. Jest did not exit one second after the test run has completed.
This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with --detectOpenHandles
to troubleshoot this issue.
[error] Jest did not exit one second after the test run has completed. 해결
import walletService from '../app/service/wallet.service.js';
import 'regenerator-runtime/runtime';
import conn from '../app/database/connection.js';
describe('address 와 blockchainId 로 wallet 조회하기', () => {
test('조회가 되는 경우', async () => {
// given
const address = '0xFDEa65C8e26263F6d9A1B5de9555D2931A300b00';
const blockchainId = 1;
try {
// when
const wallet = await walletService.findByAddressAndBlockchainId(address, blockchainId);
// then
expect(wallet.address).toEqual('0xFDEa65C8e26263F6d9A1B5de9555D2931A300b00');
expect(wallet.blockchainId).toEqual(1);
} catch (err) {
// then
expect(String(err)).toMatch("Error: [WalletService] Error finding wallet");
} finally {
// walletService.findByAddressAndBlockchainId 사용시 만들어진 DB connectino 자원 회수
conn.closeAllConnections();
}
})
})
첫 번째
두 번째
expect(() => compileAndroidCode()).toThrow(에러 내용이 들어갑니다.)
: toThrow() 을 사용할 때는 expect() 인자 값으로 function 이 들어가야 한다.Jest 강좌 #5 목 함수(Mock Functions) - 자바스크립트 테스트 프레임워크;
const mockFn = jest.fn();
mockFn();
mockFn(1); // 호출될 때 인수 전달, 여러개 가능
mockFn.mockReturnValueOnce(1);
test("mockFn 구경", () => {
console.log(mockFn);
console.log(mockFn.mock);
console.log(mockFn.mock.results);
});
test("함수는 2번 호출됩니다.", () => {
expect(mockFn.mock.calls.length).toBe(2);
});
test("2번째로 호출된 함수에 전달된 첫 번째 인수는 1 입니다.", () => {
expect(mockFn.mock.calls[1][0]).toBe(1);
});
/**
* Mock Function 으로 기능 동작하는 코드인지 확인하기
*/
const mockFn1 = jest.fn();
function forEachAdd1(arr) {
arr.forEach(num => {
// fn(num+1)
mockFn1(num + 1);
})
}
forEachAdd1([10,20,30])
test("함수 호출은 3번 됩니다.", () => {
expect(mockFn1.mock.calls.length).toBe(3);
})
test("전달된 값은 11, 21, 31 입니다.", () => {
expect(mockFn1.mock.calls[0][0]).toBe(11);
expect(mockFn1.mock.calls[1][0]).toBe(21);
expect(mockFn1.mock.calls[2][0]).toBe(31);
})
// mock function
const mockFn2 = jest.fn(num => num + 1);
mockFn2(10)
mockFn2(20)
mockFn2(30)
describe("MockFn2", () => {
test("함수 호출은 3번 됩니다.", () => {
console.log(mockFn2.mock.calls);
console.log(mockFn2.mock.results);
expect(mockFn2.mock.calls.length).toBe(3);
});
test("10에서 1 증가한 값이 반환된다", () => {
expect(mockFn2.mock.results[0].value).toBe(11);
});
test("20에서 1 증가한 값이 반환된다", () => {
expect(mockFn2.mock.results[1].value).toBe(21);
});
test("20에서 1 증가한 값이 반환된다", () => {
expect(mockFn2.mock.results[2].value).toBe(31);
});
})
/**
* Return 값 확인하기
*/
const mockFn3 = jest.fn();
mockFn3
.mockReturnValueOnce(10)
.mockReturnValueOnce(20)
.mockReturnValueOnce(30)
.mockReturnValue(40)
mockFn3();
mockFn3();
mockFn3();
mockFn3();
describe('MockFn3', () => {
test('results 확인', () => {
console.log(mockFn3.mock.results);
});
})
/**
* Mock 비동기 함수 흉내
*/
const mockFn4 = jest.fn();
mockFn4.mockResolvedValue({ name: "Mike" });
describe('MockFn4', () => {
test("받아온 이름은 Mike", () => {
mockFn4().then(res => {
expect(res.name).toBe("Mike");
});
});
})
/**
* 외부 모듈 사용하는 것 테스트 하기
*/
import fn from './fn.js';
jest.mock('./fn.js'); // mock 객체로 만들어준다.
// mocking 화를 시켜서 실제 코드를 실행시키지는 않는다.
fn.createUser.mockReturnValue({ name: "Mike" });
describe('외부 모듈 테스트', () => {
test('유저를 만든다', () => {
const user = fn.createUser("Mike");
expect(user.name).toBe("Mike");
});
});
/**
* 유용한 것
*/
const mockFn5 = jest.fn();
mockFn5(10, 20);
mockFn5();
mockFn5(30, 40);
describe('MockFn5', () => {
test("한번 이상 호출?", () => { // 호출 여부
expect(mockFn5).toBeCalled();
});
test("정확히 세번 호출?", () => { // 정확한 호출 횟수
expect(mockFn5).toBeCalledTimes(3);
});
test("10이랑 20 전달받은 함수가 있는가?", () => { // 인수로 어떤걸 받았는지 체크
expect(mockFn5).toBeCalledWith(10, 20);
expect(mockFn5).toBeCalledWith(30, 40);
});
test("마지막 함수는 30이랑 40 받았음?", () => { // 마지막에 받은 인수 체크
expect(mockFn5).lastCalledWith(30, 40);
// expect(mockFn5).lastCalledWith(10, 20); // 마지막 이기 때문에 실패한다.
});
});
console.log
[Function: mockConstructor] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
at Object.<anonymous> (test/fn.test.js:8:13)
console.log
{
calls: [ [], [ 1 ] ],
instances: [ undefined, undefined ],
invocationCallOrder: [ 1, 2 ],
results: [
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
],
lastCall: [ 1 ]
}
at Object.<anonymous> (test/fn.test.js:9:13)
console.log
[
{ type: 'return', value: undefined },
{ type: 'return', value: undefined }
]
at Object.<anonymous> (test/fn.test.js:10:13)
console.log
[ [ 10 ], [ 20 ], [ 30 ] ]
at Object.<anonymous> (test/fn.test.js:55:17)
console.log
[
{ type: 'return', value: 11 },
{ type: 'return', value: 21 },
{ type: 'return', value: 31 }
]
at Object.<anonymous> (test/fn.test.js:56:17)
console.log
[
{ type: 'return', value: 10 },
{ type: 'return', value: 20 },
{ type: 'return', value: 30 },
{ type: 'return', value: 40 }
]
at Object.<anonymous> (test/fn.test.js:89:17)
PASS test/fn.test.js
✓ mockFn 구경 (14 ms)
✓ 함수는 2번 호출됩니다. (1 ms)
✓ 2번째로 호출된 함수에 전달된 첫 번째 인수는 1 입니다.
✓ 함수 호출은 3번 됩니다. (2 ms)
✓ 전달된 값은 11, 21, 31 입니다.
MockFn2
✓ 함수 호출은 3번 됩니다. (1 ms)
✓ 10에서 1 증가한 값이 반환된다
✓ 20에서 1 증가한 값이 반환된다 (1 ms)
✓ 20에서 1 증가한 값이 반환된다
MockFn3
✓ results 확인
MockFn4
✓ 받아온 이름은 Mike
외부 모듈 테스트
✓ 유저를 만든다
MockFn5
✓ 한번 이상 호출?
✓ 정확히 세번 호출? (1 ms)
✓ 10이랑 20 전달받은 함수가 있는가?
✓ 마지막 함수는 30이랑 40 받았음?
----------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
----------|---------|----------|---------|---------|-------------------
All files | 7.69 | 100 | 0 | 7.69 |
fn.js | 7.69 | 100 | 0 | 7.69 | 2-30
----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests: 16 passed, 16 total
Snapshots: 0 total
Time: 0.288 s, estimated 1 s