입력을 가져가고 출력을 반환하는 하나의 애플리케이션을 분리된 단위로 테스트합니다.
입력 X 및 Y에 대해 출력 Z를 예상할 수 있는 구조가 명확한 함수가 있을 때 사용합니다.
애플리케이션의 모든 단위를 테스트해서 잘 작동하면 전체 애플리케이션도 작동하리라 추측할 수 있기 때문에 보통 가장 많이 사용하는 테스트입니다.
분리된 코드 조각이 아닌 다른 함수를 호출하는 함수가 있을 때 테스트하는 함수는 다른 함수의 결과에 따라 달라집니다.
단일 단위 외에도 기능을 또 다른 기능에 통합하는 것을 테스트하는 것입니다.
보통 두 개의 개별로 실행되는 단위를 통합해서 실행하기 위해 사용됩니다.
전체 애플리케이션 또는 전체 애플리케이션 일부에 대해서 테스트합니다.
브라우저에서 수동으로 원하는 작업을 하면서도 특정한 단계를 실행하는 자동화된 스크립트를 작성하여 예상한 결과를 얻는지 확인하는 것입니다.
전체 사용자 인터페이스 테스트는 가장 복잡하기 때문에 정확히 무엇이 오류를 발생시키는지 구분하기 어려운 테스트입니다.
유닛(Unit) 테스트와 통합(Integration) 테스트에서 사용되며, 테스트를 실행하고 결과를 요약해서 결과에 대한 출력을 제공합니다.
흔히 Macha와 Jest를 사용합니다.
유닛(Unit) 테스트와 통합(Integration) 테스트에서 사용되며, 테스트 논리와 예상 결과를 정의합니다.
도구를 제공하여 예상 및 테스트의 일부로써 확인하고 싶은 비교 및 조건 등을 정의합니다.
흔히 Chai와 Jest를 사용합니다.
보통 E2E 테스팅에 사용되며 Headless Browser는 코드에서 분석하기 때문에 수동으로 클릭하지 않아도 되고 브라우저 API와 DOM 등을 사용할 수 있으며, 사용자 인터페이스는 필요하지 않습니다.
흔히 Puppeteer를 사용합니다.
프로젝트의 개발 의존성이고 테스트를 실행하고 결과를 확인하거나 조건 혹은 어서션(Assertion)을 작성하는 도구입니다.
npm install --save-dev jest
"jest"로 실행하면 자동으로 .test나 .spec으로 끝나는 이름의 파일을 실행합니다
jest
"jest --watch"로 실행하면 Jest는 감시를 시작합니다.
감시가 시작되면 모든 테스트를 재실행하거나 다른 작업도 가능합니다.
(파일을 변경하고 저장하면 Jest가 자동으로 테스트를 재실행)
또한 바로가기(Watch Usage)를 사용하여 감시자를 제어할 수도 있습니다.
jest --watch
jest --watch 는 Watch Usage의 o로 테스트를 실행하게 됩니다.
o는 hg나 git에서 변경된 파일만 실행하는 옵션이기 때문에 이미 커밋된 테스트에 대해서는 실행되지 않습니다.
이럴 땐 jest --watchAll을 사용합니다.
jest --watchAll
자세한 내용은 Jest 공식 문서에서 확인할 수 있습니다.
테스트 성공과 실패의 원인이 되는 부분을 말합니다.
Jest에서 제공하며 Assertion Library에 해당합니다.
expect(linkElement).toBeInTheDocument();
expect: 단언을 시작하는데 사용
expect argument: 단언의 대상
matcher: 단언의 타입(Jest DOM에서 가져옴)
단언은 Jest의 전역 메서드인 expect로 시작합니다.
expect 다음은 Matcher가 오게 됩니다.
Matcher는 단언의 유형으로 여러가지를 사용할 수 있습니다.
위의 코드는 Jest DOM의 toBeInTheDocument를 사용했으며 toBeInTheDocument는 해당 element가 document으며 에 존재하는지 확인할 때 사용합니다.
toBeInTheDocument는 가상 DOM에만 사용할 수 있으며 모든 노드에 적용할 수 있는 toBe와 toHaveLength 같은 매처도 있습니다.
toBe(값)
: 원시형 값이 정확히 일치하는지 테스트 할 때 사용합니다.
toEqual(값)
: 참조형 값이 정확히 일치하는지 테스트 할 때 사용합니다.
toStrictEqual(값)
: toEqual과 비슷하지만 특정 요소에 undefined가 나오는 것을 허용하지 않습니다.
toBeNull()
: null에만 일치합니다.
toBeUndefined()
: undefined에만 일치합니다.
toBeDefined()
: toBeUndefined의 반대입니다.
toBeTruthy()
: if 구문의 condition이 true로 취급하는 모든 것과 일치합니다.
toBeFalsy()
: if 구문의 condition이 false로 취급하는 모든 것과 일치합니다.
if 구문의 condition이란 아래와 같습니다.
if (condition)
statement1
[else
statement2]
toBeGreaterThan(숫자)
: 인자로 들어온 숫자보다 커야합니다. (초과)
toBeGreaterThanOrEqual(숫자)
: 인자로 들어온 숫자보다 크거나 같아야 합니다. (이상)
toBeLessThan(숫자)
: 인자로 들어온 숫자보다 작아야 합니다. (미만)
toBeLessThanOrEqual(숫자)
: 인자로 들어온 숫자보다 작거나 같아야 합니다. (이하)
toBeCloseTo(숫자)
: 부동 소수점 숫자가 정확히 일치하는지 테스트 할 때 사용합니다.
부동 소수점 숫자를 toBe() 또는 toEqual()로 테스트할 경우 반올림 오류로 동작하지 않을 수 있습니다.
자바스크립트에서 0.1 + 0.2는 0.3이 아니고 0.30000000000000004으로 계산됩니다.
컴퓨터는 숫자를 계산할 때 2진법으로 계산합니다.
몇몇 소수는 10진법에서 2진법으로 변환하는 과정에서 무한 소수가 되어버립니다.
이때 저장공간의 한계가 있는 컴퓨터는 무한 소수를 유한 소수로 바꾸게 됩니다.
이 과정에서 미세한 오차가 발생하면서 값들이 손실되거나 초과하게 됩니다.
출처: harimad.log
toMatch(정규식)
: 정규식과 비교하여 문자열을 검사할 수 있습니다.
stringContaining(문자열)
: 주어지는 문자를 포함하고 있는지 테스트합니다.
toContain(특정 항목)toThrow(오류 메세지 or 정규식)toThrow assertion이 실패합니다.아래의 함수는 의존성이 없이 단순히 입력과 인자 두 개를 갖고 출력을 반환하고 있습니다.
// util.js
exports.generateText = (name, age) => {
return `${name} (${age} years old)`;
};
Jest는 자동으로 .test와 .spec이 포함된 파일을 탐지하여 실행하기 때문에 .test를 파일명에 사용하여 테스트를 작성할 파일을 만들어 줍니다.
새로 만든 파일에 테스트할 함수를 가져옵니다.
// util.test.js
const { generateText } = require("./util");
test() 함수는 Jest에서 제공하며 Test Runner에 해당합니다.
테스트를 실행할 때 사용하며 첫 번째 인자로는 텍스트로된 설명을 사용합니다.
두 번째 인자로는 테스트를 실행하기 위해 Jest가 실행할 익명함수를 전달합니다.
// util.test.js
const { generateText } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
});
비교하고 싶은 내용을 전달하고 expect(text)가 특정한 텍스트와 동일하다면 여러 헬퍼 함수를 사용합니다.
// util.test.js
const { generateText } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
테스트할 함수를 변경하여 실패로 만들어 보겠습니다.
// util.js
exports.generateText = (name, age) => {
// Returns output text
return `${age} (${age} years old)`;
};
Expected는 예상한 내용, Received는 실제 얻은 내용입니다.
만약 테스트할 함수의 반환값을 아래의 값으로 바꾼다면 실제로는 옳지 않은 함수이지만 테스트 함수를 통과하는 거짓 긍정을 피할 수 없습니다.
// util.js
exports.generateText = (name, age) => {
return "Max (29 years old)";
};
// util.test.js
const { generateText } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
이 경우 두 번째 테스트를 작성하여 반대 상황을 확인하거나 같은 내용을 다른 인자로 이중 확인할 수 있습니다.
// util.test.js
const { generateText } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
const text2 = generateText("Anna", 28);
expect(text2).toBe("Anna (28 years old)");
});
test("should output data-less text", () => {
const text = generateText();
expect(text).toBe("undefined (undefined years old)");
});
아래의 함수들은 입력이 없고 출력도 반환하지 않습니다.
대신 다른 함수를 많이 사용하여 의존성이 높으며, DOM을 추가한 함수입니다.
// app.js
const { checkAndGenerate, createElement } = require("./util");
const initApp = () => {
// app을 초기화하고 button click listener를 등록
const newUserButton = document.querySelector("#btnAddUser");
newUserButton.addEventListener("click", addUser);
};
const addUser = () => {
// 사용자 입력을 가져오고 이를 기반으로 새로운 HTML 요소를 만들고
// 요소를 DOM에 추가
const newUserNameInput = document.querySelector("input#name");
const newUserAgeInput = document.querySelector("input#age");
const outputText = checkAndGenerate(
newUserNameInput.value,
newUserAgeInput.value
);
if (!outputText) return;
const userList = document.querySelector(".user-list");
const element = createElement("li", outputText, "user-item");
userList.appendChild(element);
};
// 앱 시작
initApp();
addUser 함수는 요소 몇 가지를 선택해서 입력을 검사하고 해당 입력을 기반으로 텍스트를 생성합니다.
// app.js
const addUser = () => {
const newUserNameInput = document.querySelector("input#name");
const newUserAgeInput = document.querySelector("input#age");
// Input 입력 값 유효성 검사하고 어떤 텍스트 생성
const outputText = checkAndGenerate(
newUserNameInput.value,
newUserAgeInput.value
);
if (!outputText) return;
const userList = document.querySelector(".user-list");
// 어떤 텍스트를 사용하여 새로운 HTML 요소를 만듦
const element = createElement("li", outputText, "user-item");
// 새로운 요소를 DOM에 추가
userList.appendChild(element);
};
통합 테스트는 실제로 테스트되는 유닛에 의존하기 때문에 통합 테스트를 작성하기 전 가능한 한 제일 작은 레벨까지 자세하게 살펴봅니다.
즉, generateText와 validateInput의 유닛 테스트를 작성하고 잘 작동하는지 확인하는 것이 중요합니다.
// util.js
const generateText = (name, age) => {
// Returns output text
return `${name} (${age} years old)`;
// return "Max (29 years old)";
};
const validateInput = (text, notEmpty, isNumber) => {
// Validate user input with two pre-defined rules
if (!text) {
return false;
}
if (notEmpty && text.trim().length === 0) {
return false;
}
if (isNumber && +text === NaN) {
return false;
}
return true;
};
exports.checkAndGenerate = (name, age) => {
if (!validateInput(name, true, false) || !validateInput(age, false, true)) {
return false;
}
return this.generateText(name, age);
};
exports.generateText = generateText;
exports.validateInput = validateInput;
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
generateText 유닛 테스트와 checkAndGenerate 통합 테스트 모두 통과하였을 경우입니다.

만약 유닛 테스트한 코드는 옳바르나 통합 테스트할 코드가 잘못 작성되어 있을 경우 아래와 같은 결과를 갖게 됩니다.
// util.js
exports.checkAndGenerate = (name, age) => {
// 코드 실수
if (validateInput(name, true, false) || !validateInput(age, false, true)) {
return false;
}
return this.generateText(name, age);
};
fn.js의 fn함수는 fn.test.js에서 callback함수를 인자로 받아서 3초 후에 받아온 callback함수를 실행합니다.
callback함수는 "Mike"라는 인자를 받아오기 때문에 expect(name).toBe("Tom");는 실패가 되어야 합니다.
// fn.js
const fn = {
getName: (callback) => {
const name = "Mike";
setTimeout(() => {
callback(name);
}, 3000);
},
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", () => {
function callback(name) {
expect(name).toBe("Tom");
}
fn.getName(callback);
});
하지만 결과는 성공입니다.
이유는 callback을 호출하기 전 3초를 기다리지 않고 getName 메서드가 끝나자마자 테스트가 종료된다는 것입니다.
callback안의 expect(name).toBe("Tom") 역시 호출되지 못합니다.
test 함수에 done이라고 하는 콜백함수를 전달해 줍니다.
done이 호출되기 전까지 Jest는 테스트를 종료하지 않습니다.
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", (done) => {
function callback(name) {
try {
expect(name).toBe("Tom");
done();
} catch (error) {
done(error);
}
}
fn.getName(callback);
});
프로미스를 사용할 때는 테스트 코드에서 return을 사용해야합니다.
return 문을 생락한다면, 테스트는 getName()메서드로부터 반환된 프로미스가 resolve 되고 then()이 콜백을 실행할 기회를 가지기 이전에 종료 될 것입니다.
return 문이 있다면 then()까지 실행하고 난 후 종료됩니다.
// fn.js
const fn = {
getName: () => {
const name = "Mike";
return new Promise((res, rej) => {
setTimeout(() => {
res(name);
}, 3000);
});
},
};
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", () => {
return fn.getName().then((name) => {
expect(name).toBe("Tom");
});
});
Matcher를 사용하면 조금 더 간단하게 사용할 수 있습니다.
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", () => {
return expect(fn.getName()).resolves.toBe("Tom");
});
에러를 확인하고 싶은경우는 아래와 같습니다.
// fn.js
const fn = {
getName: () => {
const name = "Mike";
return new Promise((res, rej) => {
setTimeout(() => {
rej("[ERROR]");
}, 3000);
});
},
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", () => {
return expect(fn.getName()).rejects.toMatch("[ERROR]");
});
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", async () => {
const name = await fn.getName();
expect(name).toBe("Tom");
});
async와 await을 .resolves와 .rejects와 함께 조합할 수도 있습니다.
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", async () => {
await expect(fn.getName()).resolves.toBe("Tom");
});
// fn.test.js
const fn = require("./fn");
test("3초 후에 받아온 이름은 Mike", async () => {
await expect(fn.getName()).rejects.toThrow("[ERROR]");
});
// app.js
const { printTitle } = require("./util");
const button = document.querySelector("button");
button.addEventListener("click", printTitle);
exports.printTitle = printTitle;
// util.js
const { fetchData } = require("./http");
const loadTitle = () => {
return fetchData().then(extractedData => {
const title = extractedData.title;
const transformedTitle = title.toUpperCase();
return transformedTitle;
});
};
const printTitle = () => {
loadTitle().then((title) => {
console.log(title);
return title;
});
};
exports.printTetle = printTitle;
// http.js
const axios = require('axios');
const fetchData = () => {
return axios
.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {
return response.data;
});
};
exports.fetchData = fetchData;
printTitle 함수를 테스트를 하면 문자열이 받아야 하는데 undefined를 받았다는 않았다는 오류가 발생합니다.
이유는 테스트 함수는 해당 함수의 반환값을 비교해야하는데, printTitle 함수에서 아무것도 반환하지 않기 때문입니다.
loadTitle 함수의 프로미스를 then으로 받아 return으로 반환하고 있지만 그건 내부 함수에 대한 값이고 printTitle함수는 아무 값도 반환하고 있지 않습니다.
따라서 위의 코드에서 테스트를 진행하고 싶다면 함수에서 값을 반환하는 loadTitle 함수를 테스트하는 것이 방법이 될 수 있습니다.
// util.test.js
const { printTitle } = require("./util");
test("should print an uppercase text", () => {
expect(printTitle).toBe("DELECTUS AUT AUTEM");
});
테스트를 작성하는 동안 종종 테스트가 수행되기 전에 발생할 필요가 있는 설정 작업이 있고, 테스트가 수행 된 이후 발생해야 할 필요가 있는 마무리 작업이 있습니다.
Jest는 이를 처리하는 헬퍼 함수들을 제공합니다.
beforeEach(() => {
initializeCityDatabase();
});
afterEach(() => {
clearCityDatabase();
});
test('city database has Vienna', () => {
expect(isCity('Vienna')).toBeTruthy();
});
test('city database has San Juan', () => {
expect(isCity('San Juan')).toBeTruthy();
});
beforeEach()
: 각각의 test가 실행되기 전에 실행됩니다.
예시에서 initializeCityDatabase() 메서드는 'city database has Vienna' 테스트가 실행되기 전에 한 번, 'city database has San Juan' 테스트가 실행되기 전에 한 번 실행됩니다.
afterEach()
: 각각의 test가 실행된 후에 실행됩니다.
경우에 따라, 파일의 시작 부분에 한 번만 설정할 필요가 있습니다.
설정이 비동기인 경우 매번 시간이 오래걸릴수 있기 때문에, 인라인으로 설정 할 수 없습니다.
Jest는 이 상황을 처리하기 위한 beforeAll과 afterAll을 제공합니다.
// fn.js
const fn = {
connectUserDb: () => {
return new Promise((res) => {
setTimeout(() => {
res({ name: "Mike", age: 30, gender: "male" });
}, 500);
});
},
disconnectDb: () => {
return new Promise((res) => {
setTimeout(() => {
res();
}, 500);
});
},
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
let user;
beforeAll(async () => {
user = await fn.connectUserDb();
});
afterAll(() => {
return fn.disconnectDb();
});
test("이름은 Mike", async () => {
await expect(user.name).toBe("Mike");
});
test("나이는 30", async () => {
await expect(user.age).toBe(30);
});
test("성별은 남성", async () => {
await expect(user.gender).toBe("male");
});
beforeAll()
: test가 실행되기 전에 실행됩니다.
예시에서 connectUserDb() 메서드는 '이름은 Mike' 테스트가 실행되기 전에 한 번 실행됩니다.
afterAll()
: test가 실행된 후에 실행됩니다.
예시에서 disconnectDb() 메서드는 '성별은 남성' 테스트가 실행되기 전에 한 번 실행됩니다.
기본적으로, before와 after 블럭은 파일의 모든 테스트에 적용됩니다.
describe() 블럭을 사용하여 테스트들을 그룹핑할 수도 있습니다.
describe() 블럭 안에 있을 경우, before와 after 블럭들은 그 describe() 블럭 내부의 테스트에만 적용됩니다.
const fn = require("./fn");
let user;
beforeEach(async () => {
user = await fn.connectUserDb();
});
afterEach(() => {
return fn.disconnectDb();
});
test("이름은 Mike", async () => {
await expect(user.name).toBe("Mike");
});
test("나이는 30", async () => {
await expect(user.age).toBe(30);
});
test("성별은 남성", async () => {
await expect(user.gender).toBe("male");
});
// descirbe로 테스트들을 그룹핑
describe("Car 관련 작업", () => {
let car;
beforeEach(async () => {
car = await fn.connectUserDb();
});
afterEach(() => {
return fn.disconnectDb();
});
test("이름은 Mike", async () => {
await expect(user.brand).toBe("Mike");
});
test("나이는 30", async () => {
await expect(user.name).toBe(30);
});
test("성별은 남성", async () => {
await expect(user.color).toBe("male");
});
});
before과 after의 실행 순서는 아래와 같습니다.
beforeAll(() => console.log('1 - beforeAll')); // 1
beforeEach(() => console.log('1 - beforeEach')); // 2, 6
afterAll(() => console.log('1 - afterAll')); // 12
afterEach(() => console.log('1 - afterEach')); // 4, 10
test('', () => console.log('1 - test')); // 3
describe('Scoped / Nested block', () => {
beforeAll(() => console.log('2 - beforeAll')); // 5
beforeEach(() => console.log('2 - beforeEach')); // 7
afterAll(() => console.log('2 - afterAll')); // 11
afterEach(() => console.log('2 - afterEach')); // 9
test('', () => console.log('2 - test')); // 8
});
// 1 - beforeAll
// 1 - beforeEach
// 1 - test
// 1 - afterEach
// 2 - beforeAll
// 1 - beforeEach
// 2 - beforeEach
// 2 - test
// 2 - afterEach
// 1 - afterEach
// 2 - afterAll
// 1 - afterAll
일단 describe 블록이 완료되면 Jest는 수집 단계에서 발견한 순서대로 모든 test를 연속적으로 실행하여 각 테스트가 완료되고 정리된 후 다음 단계로 넘어가도록 기다립니다.
describe("describe outer", () => {
console.log("describe outer-a");
describe("describe inner 1", () => {
console.log("describe inner 1");
test("test 1", () => console.log("test 1"));
});
console.log("describe outer-b");
test("test 2", () => console.log("test 2"));
describe("describe inner 2", () => {
console.log("describe inner 2");
test("test 3", () => console.log("test 3"));
});
console.log("describe outer-c");
});
// describe outer-a
// describe inner 1
// describe outer-b
// describe inner 2
// describe outer-c
// test 1
// test 2
// test 3
테스트가 실패하는 경우, 가장 먼저 확인해야 할 사항 중 하나는 수행할 테스트가 유일할 경우 테스트가 실패하는가의 여부여야 합니다.
Jest에서 단 하나의 테스트만 수행하기 위해, 임시적으로 그 test 명령어를 test.only()로 변경하세요:
test.only('this will be the only test that runs', () => {
expect(true).toBe(false);
});
test('this test will not run', () => {
expect('A').toBe('A');
});
test.skip()을 사용하면 해당 test를 건너 뛸 수도 있습니다.
test.('this test will run', () => {
expect(true).toBe(false);
});
test.skip('this test will not run', () => {
expect('A').toBe('A');
});
// index.js
function areAnagrams(first, second) {
const counter = {};
for (const ch of first) {
counter[ch] = (counter[ch] || 0) + 1;
}
for (const ch of second) {
counter[ch] = (counter[ch] || 0) - 1;
}
return Object.values(counter).every((cnt) => cnt == 0);
}
areAnagrams() 함수에 여러 인자를 넣어 테스트 하려면 여러 개의 test를 사용해야합니다.
// index.test.js
import { areAnagrams } from "./";
test("car and bike are not anagrams", () => {
expect(areAnagrams("car", "bike")).toBe(false);
});
test("car and arc are anagrams", () => {
expect(areAnagrams("car", "arc")).toBe(true);
});
test("cat and dog are not anagrams", () => {
expect(areAnagrams("cat", "dog")).toBe(false);
});
test("cat and act are anagrams", () => {
expect(areAnagrams("cat", "act")).toBe(true);
});
이럴 때 test.each()를 사용하면 하나의 test만 사용할 수 있습니다.
// index.test.js
import { areAnagrams } from "./";
test.each([
["cat", "bike", false],
["car", "arc", true],
["cat", "dog", false],
["cat", "act", true],
])("areAnagrams(%s, %s) returns %s", (first, second, expected) => {
expect(areAnagrams(first, second)).toBe(expected);
test의 별칭(alias)인 it을 통해서도 동일한 방식으로 test.each() 함수를 사용할 수 있습니다.
// index.test.js
import { areAnagrams } from "./";
describe("areAnagrams()", () => {
it.each([
["cat", "bike", false],
["car", "arc", true],
["cat", "dog", false],
["cat", "act", true],
])("areAnagrams(%s, %s) returns %s", (first, second, expected) => {
expect(areAnagrams(first, second)).toBe(expected);
});
});
Mock 함수 인스턴스를 만드는 가장 간단한 방법은 jest.fn() 을 쓰는 것입니다.
const myMock = jest.fn();
Mock 함수에 인자를 넘겨 호출할 수 있습니다.
myMock();
myMock(1);
myMock("a");
myMock([1, 2], { a: "b" });
jest.fn()으로 만든 mock 함수에는 mock 프로퍼티가 있습니다.
mock 프로퍼티에는 calls라는 배열이 있습니다.
// fn.test.js
const mockFn = jest.fn();
mockFn();
mockFn(1);
// mockFn.mock은 [[], [1]]
test("함수는 2번 호출됩니다.", () => {
expect(mockFn.mock.length).toBe(2);
});
test("2번째로 호출된 함수에 전달된 첫번째 인수는 1 입니다.", () => {
expect(mockFn.mock.calls[1][0]).toBe(2);
});
// fn.test.js
const mockFn = jest.fn();
function forEachAdd1(arr) {
arr.forEach((num) => {
mockFn(num + 1);
// mockFn.mock은 [[11], [21], [31]]
});
}
forEachAdd1([10, 20, 30]);
test("함수 호출은 3번 됩니다", () => {
expect(mockFn.mock.calls.length).toBe(3);
});
test("전달된 값은 11, 21, 31", () => {
expect(mockFn.mock.calls[0][0]).toBe(11);
expect(mockFn.mock.calls[1][0]).toBe(21);
expect(mockFn.mock.calls[2][0]).toBe(31);
});
mock.results에는 return되는 값이 들어 있습니다.
// fn.test.js
const mockFn = jest.fn((num) => num + 1);
mockFn(10);
mockFn(20);
mockFn(30);
test("함수 호출은 3번 됩니다", () => {
console.log(mockFn.mock.results);
// [{ type: 'return', value: 11},
// { type: 'return', value: 21},
// { type: 'return', value: 31}]
expect(mockFn.mock.calls.length).toBe(3);
});
test("10에서 1 증가한 값이 반환된다", () => {
expect(mockFn.mock.results[0].value).toBe(11);
});
test("20에서 1 증가한 값이 반환된다", () => {
expect(mockFn.mock.results[0].value).toBe(21);
});
test("30에서 1 증가한 값이 반환된다", () => {
expect(mockFn.mock.results[0].value).toBe(31);
});
모의 함수는 테스트 중에 코드에 테스트 값을 주입할 수도 있습니다.
mockReturnValue()을 사용하면 mock 함수의 return 값을 바꿀 수 있습니다.
테스트 중간에 바꿀 때는 mockReturnValueOnce() 를 사용합니다.
// fn.test.js
const mockFn = jest.fn((num) => num + 1);
mockFn
.mockReturnValueOnce(10)
.mockReturnValueOnce(20)
.mockReturnValueOnce(30)
.mockReturnValue(40);
mockFn();
mockFn();
mockFn();
mockFn();
test("함수 호출은 3번 됩니다", () => {
console.log(mockFn.mock.results);
// [{ type: 'return', value: 10},
// { type: 'return', value: 20},
// { type: 'return', value: 30},
// { type: 'return', value: 40}]
expect("dd").toBe("dd");
});
어떤 숫자가 num으로 넘어오던지 결과가 true와 false를 번갈아가며 반환되도록 정해 놓을 수도 있습니다.
// fn.test.js
mockFn
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValueOnce(true)
.mockReturnValueOnce(false)
.mockReturnValue(true);
// [1, 3, 5]
const result = [1, 2, 3, 4, 5].filter((num) => mockFn(num));
test("홀수는 1,3,5", () => {
expect(result).toStrickEqual([1, 3, 5]);
});
mockResolvedValue() 함수를 사용하면 비동기 함수를 만들 수 있습니다.
// fn.test.js
// mockFn의 resutn은 resolve된 Promise
mockFn.mockResolvedValue({ name: "Mike" });
test("받아온 이름은 Mike", () => {
mockFn().then((res) => {
expect(res.name).toBe("Mike");
});
});
sendEmail과 sendSMS 함수를 모킹하고 싶습니다.
// messageService.js
export function sendEmail(email, message) {
/* 이메일 보내는 코드 */
}
export function sendSMS(phone, message) {
/* 문자를 보내는 코드 */
}
이때 jest.fn()에 바로 할당할 수 없습니다.
이유는 import 키워드로 불러오기 된 함수들은 기본적으로 const 변수이기 때문에 한 번 초기화되면 다른 값으로 변경이 불가능하기 때문입니다.
// userService.test.js
import { sendEmail, sendSMS } from "./messageService";
sendEmail = jest.fn(); // "sendEmail" is read-only.
sendSMS = jest.fn(); // "sendSMS" is read-only.
해결법으로는 messageService 모듈의 모든 함수를 하나의 객체로 불러오면, 객체의 속성으로 목(mock) 함수를 할당하는 것입니다.
// userService.test.js
import * as messageService from "./messageService";
messageService.sendEmail = jest.fn();
messageService.sendSMS = jest.fn();
위의 방법보다 더 나은 해결법이 있습니다.
jest.mock()은 자동으로 모듈을 모킹을 해주기 때문에 위와 같이 직접 일일이 모킹을 해줄 필요가 없습니다.
// userService.test.js
import { sendEmail, sendSMS } from "./messageService";
jest.mock("./messageService");
beforeEach(() => {
// jest.fn()없이 sendEmail, sendSMS 사용
sendEmail.mockClear();
sendSMS.mockClear();
});
jest.mock()은 첫 번째 인자로 넘어온 모듈 내의 모든 함수를 자동으로 mock 함수로 바꿔주어 작성한 코드의 return을 정해줄 수 있습니다.
// fn.js
const fn = {
createUser: (name) => {
console.log("사용자가 생성되었습니다.");
return {
name,
};
},
};
module.exports = fn;
// fn.test.js
const fn = require("./fn");
// fn함수를 mock 함수로 바꿈
jest.mock("./fn");
// mock 함수로 바뀐 fn함수의 return을 고정
fn.createUser.mockReturnValue({ name: "Mike" });
test("create user", () => {
const user = fn.createUser("Mike");
expect(user.name).toBe("Mike");
});
jest.fn에서 구현하는 것과 같습니다.
차이점은 모의 개체(mockFn)가 호출될 때 구현도 실행합니다.
const mockFn = jest.fn();
mockFn.mockImplementation(scalar => 42 + scalar);
mockFn(0); // 42
mockFn(1); // 43
const mockFn = jest.fn(scalar => 42 + scalar);
mockFn(0); // 42
mockFn(1); // 43
const mockFn = jest.fn();
mockFn(10, 20);
mockFn();
mockFn(30, 40);
test("한번 이상 호출되면 통과", () => {
expect(mockFn).toBeCalled();
});
test("인자의 숫자만큼 호출되면 통과", () => {
expect(mockFn).toBeCalledTimes(3);
});
test("인자의 값을 전달 받은 함수가 있다면 통과", () => {
expect(mockFn).toBeCalledWith(10, 20);
});
test("마지막으로 실행된 함수가 인자의 값을 전달 받았다면 통과", () => {
expect(mockFn).laseCalledWith(30, 40);
});
어떤 객체에 속한 함수의 구현을 가짜로 대체하지 않고, 해당 함수의 호출 여부와 어떻게 호출되었는지만을 알아내야 할 때 jest.spyOn()을 사용합니다.
jest.spyOn(object, "methodName");
const calculator = {
add: (a, b) => a + b,
};
// calculator 객체의 add라는 함수에 스파이를 붙였습니다.
const spyFn = jest.spyOn(calculator, "add");
const result = calculator.add(2, 3);
// add 함수의 호출 횟수가 1이면 통과
expect(spyFn).toBeCalledTimes(1);
// add 함수 인자에 2, 3이 넘어가면 통과
expect(spyFn).toBeCalledWith(2, 3);
// add 함수의 반환이 5라면 통과
expect(result).toBe(5);
// app.js
const { printTitle } = require("./util");
const button = document.querySelector("button");
button.addEventListener("click", printTitle);
exports.printTitle = printTitle;
// util.js
const { fetchData } = require("./http");
const loadTitle = () => {
return fetchData().then(extractedData => {
const title = extractedData.title;
const transformedTitle = title.toUpperCase();
return transformedTitle;
});
};
const printTitle = () => {
loadTitle().then((title) => {
console.log(title);
return title;
});
};
exports.printTetle = printTitle;
// http.js
const axios = require('axios');
const fetchData = () => {
return axios
.get('https://jsonplaceholder.typicode.com/todos/1')
.then(response => {
return response.data;
});
};
exports.fetchData = fetchData;
loadTitle 함수는 API를 사용하고 있습니다.
http 요청을 사용하는 코드에서는 http 요청을 만드는 것이 좋습니다.
이유는 프론트엔드의 테스트 목적은 API가 올바르게 작동하는지가 아니기 때문입니다.
특히 axios에 의해 노출된 get 메서드의 작동 여부도 테스트하지 않는 게 좋습니다.
axios는 제3자 패키지에 의해 제공되며 우린 그 패키지의 역할에 의존합니다.
그러므로 제3자 패키지의 기능 역시 테스트하지 않습니다.
__mocks 폴더를 만들어 http.js 파일에 mock 코드를 작성합니다.
// __mocks__/http.js
const fetchData = () => {
return Promise.resolve({ title: "delectus aut autem" });
};
exports.fetchData = fetchData;
jest.mock("./http"); 코드를 작성하여 http.js 파일을 대체합니다.
// util.test.js
jest.mock("./http");
const { loadTitle } = require("./util");
test("should print an uppercase text", () => {
loadTitle().then((title) => {
expect(title).toBe("DELECTUS AUT AUTEM");
});
});
__mocks 폴더를 만들어 axios.js 파일에 mock 코드를 작성합니다.
// axios.js
const get = (url) => {
return Promise.resolve({ data: { title: "delectus aut autem" } });
};
exports.get = get;
axios는 jest.mock()을 사용하지 않아도 됩니다.
Jest는 node_modules 폴더에서 외부 모듈을 자동 모킹할 수 있습니다.
const { loadTitle } = require("./util");
test("should print an uppercase text", () => {
loadTitle().then((title) => {
expect(title).toBe("DELECTUS AUT AUTEM");
});
});
기본적으로는 DOM 등과 소통할 때 사용 가능한 Chrome 브라우저의 헤드리스 버전입니다.
헤드가 있는 버전에서 실행할 수도 있습니다.
npm install --save-dev puppeteer
Puppeteer를 임포트하고 puppeteer.launch를 사용하여 브라우저를 구축, 생성합니다.
puppeteer.launch에서는 정의하는 옵션과 함께 브라우저를 실행하게 됩니다.
// util.test.js
const puppeteer = require("puppeteer");
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should click around", () => {
const browser = puppeteer.launch({
// 사용자 인터페이스로도 브라우저를 실행
headless: false,
// 자동화된 작업의 처리 속도 늦춤
slowMo: 80,
// 실행할 브라우저를 이 창의 크기 1920,1080
args: ["--window-size=1920,1080"],
});
});
브라우저와 관련된 것들은 실행에 시간이 걸리는 프로미스들이라 각 단계에 await을 해줘야합니다.
await browser.newPage를 통해 페이지 객체를 생성합니다.
page.goto 로드 되어야 하는 URL을 넣어주어 브라우저에게 어떤 페이지를 불러올 건지 알려줍니다.
// util.test.js
const puppeteer = require("puppeteer");
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should click around", async () => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: ["--window-size=1920,1080"],
});
const page = await browser.newPage();
await page.goto("file:/Volumes/projects/test/index.html");
});
jest watch를 통해 테스트를 실행하면 test 브라우저(Chromium)가 실행됩니다.
Chromium은 기본적으로 Chrome의 핵심 기능과 비슷하며 테스트 소프트웨어에 의해 테스트나 제어가 됩니다.
page.click()으로 해당 선택자 요소를 클릭하고 page.type()으로 해당 요소에 두 번째 인자 값을 입력하고 input에 값들이 입력되면 page.click으로 AddUser 버튼을 클릭하는 논리를 추가합니다.
논리를 추가 하고 테스트를 실행하면 puppeteer가 Chromium에 해당 논리를 수행해 줍니다.
const puppeteer = require("puppeteer");
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should click around", async () => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: ["--window-size=1920,1080"],
});
const page = await browser.newPage();
await page.goto(
"file:/Volumes/projects/test/index.html"
);
await page.click("input#name");
await page.type("input#name", "Anna");
await page.click("input#age");
await page.type("input#age", "29");
await page.click("#btnAddUser");
});
또한 Add User버튼 클릭으로 생기는 list의 결과도 테스트 해볼 수 있습니다.
논리가 추가되면 타임아웃이 걸릴 수 있기 때문에 test()의 세 번째 인자로 최대 타임아웃을 늘려줍니다.
const puppeteer = require("puppeteer");
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should click around", async () => {
const browser = await puppeteer.launch({
headless: false,
slowMo: 80,
args: ["--window-size=1920,1080"],
});
const page = await browser.newPage();
await page.goto(
"file:/Volumes/projects/udemy/javascript-maximilian/section31/testing-01-starting-setup/index.html"
);
await page.click("input#name");
await page.type("input#name", "Anna");
await page.click("input#age");
await page.type("input#age", "29");
await page.click("#btnAddUser");
// user-item을 찾아가서 textContent를 반환
const finalText = await page.$eval(".user-item", (el) => el.textContent);
// 반환된 값이 "Anna (29 years old)" 와 같은지 확인
expect(finalText).toBe("Anna (29 years old)");
// 타임아웃을 10000ms로 늘려줌
}, 10000);
headless 옵션을 true로 하고 slowMo,args 옵션을 사용하지 않으면 시각적으로 보이지는 않지만 실제 브라우저에서는 과정을 전부 거치지 않아도 되기 때문에 테스트의 속도가 빨라질 수 있습니다.
const puppeteer = require("puppeteer");
const { generateText, checkAndGenerate } = require("./util");
test("should output name and age", () => {
const text = generateText("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should generate a valid text output", () => {
const text = checkAndGenerate("Max", 29);
expect(text).toBe("Max (29 years old)");
});
test("should create an element with text and correct class", async () => {
const browser = await puppeteer.launch({
headless: true,
// slowMo: 80,
// args: ["--window-size=1920,1080"],
});
const page = await browser.newPage();
await page.goto(
"file:/Volumes/projects/udemy/javascript-maximilian/section31/testing-01-starting-setup/index.html"
);
await page.click("input#name");
await page.type("input#name", "Anna");
await page.click("input#age");
await page.type("input#age", "29");
await page.click("#btnAddUser");
const finalText = await page.$eval(".user-item", (el) => el.textContent);
expect(finalText).toBe("Anna (29 years old)");
}, 10000);