
앞서 학습한 리펙토링과 테스트코드에 이어 테스트 코드의 결과를 쉽게 확인하기 위한 장치인
테스트 프레임워크에 대해 알아보고 사용해보자.
자바스크립트의 테스트 프레임워크에는 다양한 종류가 있다.
Jest는 zero config 철학으로 별도의 설정없이 테스트 코드를 빠르게 작성할 수 있다.
"Zero Configuration"는 소프트웨어 개발에서 자동화를 강조하는 철학이다.
이 철학은 사용자가 소프트웨어를 설정하거나 구성하는 데 필요한 번거로움을 최소화하고,
개발자가 가능한 한 적은 수의 명령어나 설정으로 개발에 집중할 수 있도록 한다.
npm install jest --save-dev
// --save-dev (개발에서만 사용할때)
// package.json 수정
// "test" : "jest" 로 수정
{
"name": "jest_study",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest"
},
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.5.0"
}
}
// fn.js
const fn = {
add: (num1, num2) => num1 + num2,
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
test("1은 1이야", () => {
expect(1).toBe(1);
});
test("2 더하기 3은 5야", () => {
expect(fn.add(2, 3)).toBe(5);
});
// 테스트를 위한 오류값 입력
test("3 더하기 3은 6야", () => {
expect(fn.add(3, 3)).toBe(6);
});
npm test로 실행 해준다.

테스트 결과를 확인 할수 있다.
테스트코드에서 사용된
toBe()를Matchers라 한다.
Jest는 Matchers를 사용하여 다양한 테스트를 할수 있다.
원시값은 toBe() 매처로 테스트가 가능하다.
하지만 객체나 배열의 경우 재귀적으로 돌면서 값을 확인하기 때문에 일치하는 값이라도 테스트시 실패가 된다.
// fn.js
const fn = {
add: (num1, num2) => num1 + num2,
makeUser: (name, age) => ({ name, age }),
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
test("이름과 나이를 전달 받아서 객체를 반환해줘", () => {
expect(fn.makeUser("kim", 30)).toBe({
name: "kim",
age: 30,
});
});

객체의 동일성을 확인하기 위해서는 toEqual() 매처를 사용한다.
const fn = require("./fn");
test("이름과 나이를 전달 받아서 객체를 반환해줘", () => {
expect(fn.makeUser("kim", 30)).toEqual({
name: "kim",
age: 30,
});
});
// FAIL
하지만, toEqual()은 객체의 undefined 키값을 무시한다.
더욱 정확한 테스트를 위해서는toStrictEqual()을 사용하는 것이 좋다.
// fn.js
const fn = {
add: (num1, num2) => num1 + num2,
makeUser: (name, age) => ({ name, age, gender: undefined }),
};
// fn.test.js
const fn = require("./fn");
test("이름과 나이를 전달 받아서 객체를 반환해줘", () => {
expect(fn.makeUser("kim", 30)).toStrictEqual({
name: "kim",
age: 30,
});
});
// PASS
test("null", () => {
const n = null;
expect(n).toBeNull();
expect(n).toBeDefined();
expect(n).not.toBeUndefined();
expect(n).not.toBeTruthy();
expect(n).toBeFalsy();
});
// PASS
test("zero", () => {
const z = 0;
expect(z).not.toBeNull();
expect(z).toBeDefined();
expect(z).not.toBeUndefined();
expect(z).not.toBeTruthy();
expect(z).toBeFalsy();
});
// PASS
숫자를 비교하는 다양한 매처
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);
});
// PASS
사용자가 입력한 id 길이를 제한하거나 업로드된 파일 크기가 적당한지 판별할 때 사용가능하다.
test("ID는 10자 이하여야 합니다.", () => {
const id = "THE_BLACK_ORDER";
expect(id.length).toBeLessThanOrEqual(10);
});
// FAIL
test("ID는 10자 이하여야 합니다.", () => {
const id = "THE_BLACK";
expect(id.length).toBeLessThanOrEqual(10);
});
// PASS
소숫점의 경우 toEqual이 아닌 toBeCloseTo를 사용하자.
tiny rounding error가 발생할 수 있다.
test("0.1 더하기 0.2는 0.3 입니다.", () => {
expect(fn.add(0.1, 0.2)).toBe(0.3);
});
// FAIL
test("0.1 더하기 0.2는 0.3 입니다.", () => {
expect(fn.add(0.1, 0.2)).toBeCloseTo(0.3);
});
// PASS
문자열은 정규 표현식과 toMatch()를 통해 테스트가 가능하다.
test("there is no I in team", () => {
expect("team").not.toMatch(/I/);
});
// PASS
test('but there is a "stop" in Christoph', () => {
expect("Christoph").toMatch(/stop/);
});
// PASS
배열에서 특정 요소가 있는지 확인하기 위해서는 toContain()을 사용한다.
const shoppingList = ["trash bags", "paper towels", "milk"];
test("the shopping list has milk on it", () => {
expect(shoppingList).toContain("milk");
expect(new Set(shoppingList)).toContain("milk");
});
// PASS
어떤 함수를 실행했을때 예외가 발생하는지 확인하기 위해서는 toThrow()를 사용한다.
함수가 Error 객체를 throw하는지 검사한다.
const fn = {
compileAndroidCode: () => {
throw new Error("you are using the wrong JDK!");
},
};
test("compiling android goes as expected", () => {
expect(() => fn.compileAndroidCode()).toThrow();
expect(() => fn.compileAndroidCode()).toThrow(Error);
// pass
// You can also use a string that must be contained in the error message or a regexp
expect(() => fn.compileAndroidCode()).toThrow("you are using the wrong JDK");
expect(() => fn.compileAndroidCode()).toThrow(/JDK/);
// pass
// Or you can match an exact error message using a regexp like below
expect(() => fn.compileAndroidCode()).toThrow(
/^you are using the wrong JDK$/
); // Test fail
expect(() => fn.compileAndroidCode()).toThrow(
/^you are using the wrong JDK!$/
); // Test pass
});
// fn2.js
const fn = {
getName: (callBack) => {
const name = "kim";
setTimeout(() => {
callBack(name);
}, 3000);
},
};
// fn2.test.js
const fn = require("./fn2");
test("3초 후에 받아온 이름은 kim", () => {
function callBack(name) {
expect(name).toBe("kim");
}
fn.getName(callBack);
});

결과는 나왔지만, 3초 후가 아닌 약 0.1초 후에 결과가 나왔다.
비동기 함수인 callBack()은 일어나지 않고 fn.getName()에서 실행이 종료되기 때문이다.
const fn = require("./fn2");
test("3초 후에 받아온 이름은 kim", (done) => {
function callBack(name) {
expect(name).toBe("kim");
done(); // 전달한 콜백함수가 실행되어야 한다.
}
fn.getName(callBack);
});
테스크 함수에
done이라는 콜백함수를 전달해주면 콜백함수 done이 실행될 때 까지 테스트 함수가 종료되지 않는다.

약 3초 후에 테스트가 잘 통과 됨을 확인할 수 있다.
// fn2.js
const fn = {
getName: (callBack) => {
const name = "kim";
setTimeout(() => {
// callBack(name);
throw new Error("서버에러..");
}, 3000);
},
};
// fn2.test.js
const fn = require("./fn2");
test("3초 후에 받아온 이름은 kim", (done) => {
function callBack(name) {
try {
expect(name).toBe("kim");
done();
} catch (error) {
done();
}
}
fn.getName(callBack);
});

// fn2.js
const fn = {
getAge: () => {
const age = 30;
return new Promise((res, rej) => {
setTimeout(() => {
res(age);
}, 3000);
});
},
};
// fn2.test.js
const fn = require("./fn2");
// return 이 반드시 존재해야 한다.
test("3초 후에 받아온 나이는 30", () => {
return fn.getAge().then((age) => {
expect(age).toBe(30);
});
});
// PASS
Promise를 사용하면 Jest에서는 콜백함수 실행까지 기다려준다.
즉, test함수에 콜백함수 done을 전달할 필요가 없다.
다만, Promise로 전달되었기에 test함수 내에서 return이 되도록 주의하자.
Promise 테스크코드에 resolves, rejects 매처를 사용하면 더 간단하게 표현이 가능하다.
const fn = require("./fn2");
// resolves 매처 사용
test("3초 후에 받아온 나이는 30", () => {
// return fn.getAge().then((age) => {
// expect(age).toBe(30);
// });
return expect(fn.getAge()).resolves.toBe(30);
});
// PASS
// fn2.js
const fn = {
getAge: () => {
const age = 30;
return new Promise((res, rej) => {
setTimeout(() => {
rej("error");
}, 3000);
});
},
};
// fn2.test.js
const fn = require("./fn2");
// rejects 매처 사용
test("3초 후에 에러가 나온다.", () => {
return expect(fn.getAge()).rejects.toMatch("error");
});
// PASS
위 예제 Promise 코드에서 async, await 코드로 변경만 해주면 된다.
const fn = require("./fn2");
test("3초 후에 받아온 나이는 30", async () => {
const age = await fn.getAge();
expect(age).toBe(30);
});
// PASS
// resolves 매처 사용
test("3초 후에 받아온 나이는 30", async () => {
await expect(fn.getAge()).resolves.toBe(30);
});
테스트를 진행할 때 테스트 전/후에 처리되어야 하는 경우가 발생할 수 있다.
아래 예제를 보자.
const fn = require("./fn2");
let num = 0;
test("0더하기 1은 1이야", () => {
num = fn.add(num, 1);
expect(num).toBe(1);
});
test("0더하기 2는 2야", () => {
num = fn.add(num, 2);
expect(num).toBe(2);
});
test("0더하기 3은 3이야", () => {
num = fn.add(num, 3);
expect(num).toBe(3);
});
테스트 함수의 값으 문제가 없으므로 모두 PASS가 나와야 하지만,
결과는 FAIL이 된다.

앞선 테스트 함수에서 변수 num값이 재할당 되기 때문이다.
이런 문제를 방지하기 위해 Jest는 헬퍼함수를 제공해준다.
테스트 하나가 실행된 후 헬퍼함수 내부의 값을 초기화 해준다.
// 초기 num 값
let num = 10;
// 모든 테스트 전에 num값을 0으로 초기화
beforeEach(() => {
num = 0;
});
test("0더하기 1은 1이야", () => {
num = fn.add(num, 1);
expect(num).toBe(1);
});
test("0더하기 2는 2야", () => {
num = fn.add(num, 2);
expect(num).toBe(2);
});
test("0더하기 3은 3이야", () => {
num = fn.add(num, 3);
expect(num).toBe(3);
});

// 첫 테스트 이후 num값을 0으로 초기화
let num = 10
afterEach(() => {
num = 0;
});
test("0더하기 1은 1이야", () => {
num = fn.add(num, 1);
expect(num).toBe(1);
});
test("0더하기 2는 2야", () => {
num = fn.add(num, 2);
expect(num).toBe(2);
});
test("0더하기 3은 3이야", () => {
num = fn.add(num, 3);
expect(num).toBe(3);
});

각 테스트 케이스마다 실행하는 헬퍼함수가 아닌 전체(코드블럭) 테스트 케이스 시작 전, 후를 설정하는 헬퍼 함수다.
beforeAll()은 테스트 스위트 전체에서 가장 처음 실행된다.
이 함수를 사용하면 모든 테스트가 실행되기 전에 필요한 전역 상태를 설정하거나 데이터베이스 연결 등의 작업을 수행할 수 있다.
beforeAll() 함수는 모든 테스트 케이스에서 공통적으로 사용되는 코드를 작성할 때 유용하다.
afterAll()은 테스트 스위트 전체에서 가장 마지막에 실행된다.
이 함수를 사용하면 모든 테스트가 실행된 후에 필요한 정리 작업을 수행할 수 있다.
데이터베이스 연결을 종료하거나 파일을 삭제하는 등의 작업을 수행할 수 있습니다.
// beforEach, afterEach를 사용하는 경우
const fn = require("./fn2");
let user;
beforeEach(async () => {
user = await fn.connectUserDb();
});
afterEach(() => {
return fn.disconnectDb();
});
test("이음은 kim", () => {
expect(user.name).toBe("kim");
});
test("나이는 30", () => {
expect(user.age).toBe(30);
});
test("성별은 남자", () => {
expect(user.gender).toBe("male");
});
// 매 test case 마다 Db데이터를 받아와서 실행한다.
// 결과는 PASS지만 3초 이상의 시간이 걸린다.
// beforeAll, afterAll를 사용하는 경우
let user;
beforeAll(async () => {
user = await fn.connectUserDb();
});
afterAll(() => {
return fn.disconnectDb();
});
test("이음은 kim", () => {
expect(user.name).toBe("kim");
});
test("나이는 30", () => {
expect(user.age).toBe(30);
});
test("성별은 남자", () => {
expect(user.gender).toBe("male");
});
// user 데이터를 한번 받아오고 하위 테스트 케이스를 실행한다.
// PASS, TIME = 1s
describe() 함수는 테스트 스위트(블록)을 생성하고, 그 안에 포함되는 test()나 it() 함수를 사용하여 테스트 케이스를 작성한다.
describe('Calculator', () => {
let calculator;
beforeEach(() => {
// 각각의 테스트 케이스에서 Calculator 인스턴스를 사용하기 위해 인스턴스 생성
calculator = new Calculator();
});
describe('addition', () => {
test('adds 1 + 2 to equal 3', () => {
expect(calculator.add(1, 2)).toBe(3);
});
test('adds 0 + 0 to equal 0', () => {
expect(calculator.add(0, 0)).toBe(0);
});
});
describe('subtraction', () => {
test('subtracts 4 - 2 to equal 2', () => {
expect(calculator.subtract(4, 2)).toBe(2);
});
test('subtracts 0 - 0 to equal 0', () => {
expect(calculator.subtract(0, 0)).toBe(0);
});
});
});