테스트 커버리지
유닛 테스트를 작성하다 보면, 전체 코드 중에서 어떤 부분이 테스트되고 어떤 부분이 테스트되지 않는지 궁금합니다. 어떤 부분이 테스트되지 않는지를 알아야 나중에 그 부분의 테스트 코드를 작성할 수 있습니다. 전체 코드 중에서 테스트되고 있는 코드의 비율과 테스트되고 있지 않은 코드의 위치를 알려주는 jest의 기능이 있습니다. 바로 커버리지(coverage) 기능입니다.
커버리지 기능을 사용하기 위해 package.json에 jest 설정을 입력합니다.
{
"name": "nodebird",
"version": "0.0.1",
"description": "익스프레스로 만드는 SNS 서비스",
"main": "app.js",
"scripts": {
"start": "nodemon app",
"test": "jest",
"coverage": "jest --coverage"
},
"author": "cherry",
"license": "MIT",
"dependencies": {
"bcrypt": "^6.0.0",
"cookie-parser": "^1.4.7",
"dotenv": "^17.2.3",
"express": "^5.1.0",
"express-session": "^1.18.2",
"morgan": "^1.10.1",
"multer": "^2.0.2",
"mysql2": "^3.15.1",
"nunjucks": "^3.2.4",
"passport": "^0.7.0",
"passport-kakao": "^1.0.1",
"passport-local": "^1.0.0",
"sequelize": "^6.37.7",
"sequelize-cli": "^6.6.3"
},
"devDependencies": {
"jest": "^24.9.0",
"nodemon": "^3.1.10"
}
}
jest 명령어 뒤에 --coverage 옵션을 붙이면 jest가 테스트 커버리지를 분석합니다.
$ npm run coverage
> nodebird@0.0.1 coverage
> jest --coverage
(node:2036) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
(node:2544) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead.
(Use `node --trace-deprecation ...` to show where the warning was created)
PASS routes/middlewares.test.js (7.592s)
PASS controllers/user.test.js (10.972s)
● Console
console.error controllers/user.js:13
테스트용 에러
-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 84 | 100 | 60 | 84 | |
controllers | 100 | 100 | 100 | 100 | |
user.js | 100 | 100 | 100 | 100 | |
models | 33.33 | 100 | 0 | 33.33 | |
user.js | 33.33 | 100 | 0 | 33.33 | 5,44,45,50 |
routes | 100 | 100 | 100 | 100 | |
middlewares.js | 100 | 100 | 100 | 100 | |
-----------------|----------|----------|----------|----------|-------------------|
Test Suites: 2 passed, 2 total
Tests: 7 passed, 7 total
Snapshots: 0 total
Time: 24.083s
Ran all test suites.
테스트 결과가 출력되고, 추가적으로 표가 하나 더 출력됩니다. 표의 열을 살펴보면, 각각 File(파일과 폴더 이름), % Stmts(구문 비율), % Branch(if문 등의 분기점 비율), % Funcs(함수 비율), % Lines(코드 줄 수 비율), Uncovered Line #s(커버되지 않은 줄 위치)입니다. 퍼센티지가 높을수록 많은 코드가 테스트되었다는 뜻입니다.
표를 보면, 전체 파일(All files) 중에서는 83.33%의 구문과 83.33%의 분기점, 60%의 함수, 83.33%의 코드 줄이 커버되었음을 알 수 있습니다. 여기서는 명시적으로 테스트하고 require한 코드만 커버리지 분석이 된다는 점에 주의해야 합니다. All files라 하더라도 현재 controllers/user.js, models/user.js, routes/middlewares.js만 포함되어 있습니다. 따라서 테스트 커버리지가 100%라 하더라도 실제로 모든 코드를 테스트한 것은 아닐 수 있습니다.
models/user.js에서는 33.33%의 구문과 100%의 분기점, 0%의 함수, 33.33%의 코드 줄이 커버되었음을 보여줍니다. 이 줄들을 아래로 표시해보았습니다. 또한 5, 41, 42, 47번째 줄은 테스트되지 않았다는 것을 보여줍니다.
코드에서 굵게 표시해봤습니다.
models/users.js
const Sequelize = require("sequelize");
module.exports = class User extends Sequelize.Model {
static init(sequelize) {
return super.init(
{
email: {
type: Sequelize.STRING(40),
allowNull: true,
unique: true,
},
nick: {
type: Sequelize.STRING(15),
allowNull: false,
},
password: {
type: Sequelize.STRING(100),
allowNull: true,
},
provider: {
type: Sequelize.STRING(10),
allowNull: false,
defaultValue: "local",
},
snsId: {
type: Sequelize.STRING(30),
allowNull: true,
},
},
{
sequelize,
timestamps: true,
underscored: false,
modelName: "User",
tableName: "users",
paranoid: true,
charset: "utf8",
collate: "utf8_general_ci",
}
);
}
static associate(db) {
db.User.hasMany(db.Post);
db.User.belongsToMany(db.User, {
foreignKey: "followingId",
as: "Followers",
through: "Follow",
});
db.User.belongsToMany(db.User, {
foreignKey: "followerId",
as: "Followings",
through: "Follow",
});
}
};
5, 41, 42, 47번째 줄에는 함수 호출이 위치하고 있습니다. 이 부분은 테스트를 하나도 작성하지 않았으므로 % Funcs가 0%로 나오는 것입니다. 테스트 커버리지를 올리기 위해 테스트를 작성해 봅시다.
const Sequelize = require("sequelize");
const User = require("./user");
const config = require("../config/config")["test"];
const sequelize = new Sequelize(
config.database,
config.username,
config.password,
config
);
describe("User 모델", () => {
test("static init 메서드 호출", () => {
expect(User.init(sequelize)).toBe(User);
});
test("static associate 메서드 호출", () => {
const db = {
User: {
hasMany: jest.fn(),
belongsToMany: jest.fn(),
},
Post: {},
};
User.associate(db);
expect(db.User.hasMany).toHaveBeenCalledWith(db.Post);
expect(db.User.belongsToMany).toHaveBeenCalledTimes(2);
});
});
각각 init과 associate 메서드가 제대로 호출되는지 테스트해봤습니다. db 객체는 모킹했습니다. 테스트를 수헹하면 성공합니다.
$ npm test
> nodebird@0.0.1 test
> jest
테스트 커버리지도 살펴봅시다. 커버리지 표 부분만 확인해보겠습니다.
$ npm run coverage
-----------------|----------|----------|----------|----------|-------------------|
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s |
-----------------|----------|----------|----------|----------|-------------------|
All files | 84 | 100 | 100 | 84 | |
controllers | 100 | 100 | 100 | 100 | |
user.js | 100 | 100 | 100 | 100 | |
models | 100 | 100 | 100 | 100 | |
user.js | 100 | 100 | 100 | 100 | |
routes | 100 | 100 | 100 | 100 | |
middlewares.js | 100 | 100 | 100 | 100 | |
-----------------|----------|----------|----------|----------|-------------------|
테스트 커버리지가 대폭 올라간 것을 볼 수 있습니다. 현재 테스트 커버리지가 100%이지만 모든 코드가 테스트되고 있는 상황은 아닙니다. 따라서 테스트 커버리지를 높이는 것에 너무 집착하기 보다는 필요한 부분 위주로 올바르게 테스트하는 것이 좋습니다.