얼마 전 Perfitt 앱의 이벤트 시스템을 개편해서 Production 릴리즈를 했습니다. 거기에 버그 하나가 있었고 서버를 Production으로 보내자마자 엄청나게 많은 비정상 앱 종료 알림들이 오더군요 ㅎㅎ..
식은 땀이 나고 호흡이 거칠어지고 사고가 둔해지고 시야가 흐릿해지는 아찔한 경험이었습니다.
정말 다신 하고 싶지 않은 경험이었구요.망치로 한 대 맞은 느낌으로 반성 및 회고를 하면서 '어떻게 하면 프로덕션에서 버그를 안 낼까' 이런 아주 기초적이면서도 필수적인 고민들을 많이 했습니다.
그래서 도달한 결론은 API 테스팅입니다. 개발자들은 자신들의 코드를 여러가지 경우에 따라 값을 테스트하는 테스트 코드를 작성하고 추후에 나타날 수 있는 버그들을 미리 잡기 위해서 많은 테스트를 합니다.
이 글에서는 아찔한 버그 생성 경험을 통해 깨달은 삶의 레슨과 현재 펄핏에서 제가 맡은 백엔드 부분에서의 테스트 자동화 구현 방법을 다뤄보려 합니다.
사실 왜 해야 하냐고 하면 제 대답은 간단합니다.
"모든 것에 확실하기 위해서"
저는 빠르게 개발하는 것에 초점을 맞춰왔던 것 같습니다.
빠르게 대응하고, 빠르게 기능을 추가하고, 빠르게 개선하고, 이런 점들이 끊임없이 변화하고 성장하는 스타트업에서 더 중요한 건 개발자의 실력이 아닌가? 라고 생각합니다.
이러한 생각에는 변함이 없습니다만 요즘 들어 제 동료 개발자분들과 협업하면서 크게 배우고 느끼는 것이 있다면 이 것입니다.
확실하게 개발해야 하는 이유
제가 맡은 일, 제가 개발한 내용들에는 저 자신이 확실해야 합니다. 그래야 협업의 과정에서 개발적인 나의 의도와 내용을 확실히 전달하고 협업할 수 있습니다. 프론트 개발자와 무조건 커뮤니케이션을 잘 해야하는 백엔드 개발자의 경우 이건 필수라고 생각합니다.
이 관점에서 더 나아가 신뢰성과도 연결된다고 생각합니다. 내가 만든 결과가 확실하지 않은데 어떻게 협업하는 동료분들이 제 파트에 신뢰를 하겠습니까?
확실하게 개발한다는 것은 나 자신이 내가 만든 코드에 확신이 있고 동료분들이 제가 만든 코드에 믿음이 생기도록 개발해 나간다는 것 같습니다.
그래서 제 코드에 확신을 가지고 동료분들에게도 제 코드에 대한 확신이 생기게끔 API 테스팅이란 하나의 도구를 사용해 보려 합니다.
Mocha
현재 펄핏의 모바일 서비스 백엔드는 Express로 구현되어 있고 Express 앱을 테스팅하기 위한 프레임워크로 Mocha를 사용하였습니다.
Javascript 테스팅 툴 같은 경우 유명한 Jest나 다른 라이브러리들이 많이 있지만 Mocha는 상대적으로 세팅이 쉽고 빠르며, 확장성이 좋고 간단한 비동기 처리 등의 장점이 있기 때문에 선택하였습니다.
Chai
Chai JS는 Assertion 라이브러리로, 테스팅할 때의 문법을 제공합니다. Assert, Should, Expect 등 세 종류의 문법을 제공하는데, 사용자의 입맛에 맞춰 사용하면 되고 문법이 언어적으로 구현되어 있어 이해하기 쉽고 사용하기 편합니다.
Chai-HTTP
Chai-HTTP는 Chai의 애드온 같은 느낌으로, Chai Test 시 HTTP Request를 보내는 형식으로 테스트를 가능하게 합니다. 구현하는 API Testing 에서는 각 Function 단위를 테스트하는 게 아닌 각 API Route Call에 대한 Test를 하려고 할 때 유용할 것으로 생각되어 선택하였습니다.
Mongo-unit
펄핏은 데이터베이스로 MongoDB를 사용하고 있는데 MongoDB에 알맞는 테스팅 방법을 찾아보다가 Mongo-unit을 발견하였습니다.
API 테스트를 할 때 실제 데이터나 개발 데이터는 가급적 조작하지 않는게 좋습니다.
데이터를 복제하는 테스트용 DB를 아예 새로 만들고 테스트 시작 시에 트랜젝션 설정 후 테스트를 완료한 뒤 다시 롤백하는 방법을 사용할 수도 있지만 안전한 테스트를 위해 다른 방법을 찾아본 결과, 테스트 시에만 인 메모리로 형성되는 DB인 Mongo-unit 을 사용하기로 하였습니다.
새로 DB를 만들 필요도 없고, Mocha와 같이 각 테스트 별로 데이터를 셋팅 하는 경우 구현이 쉬운 장점이 있습니다.
필요한 devDependencies 정의
"devDependencies": {
"mocha": "^8.4.0", // Mocha 프레임워크
"chai": "^4.3.4", // assertion 라이브러리
"chai-http": "^4.3.0", // chai 에서 http 콜 가능하게함
"sinon": "^11.1.1", // mocking library
"mongo-unit": "^2.0.1" // 인메모리 몽고디비
}
필요한 라이브러리를 설치합니다.
아래 코드로 한번에 설치할 수 있습니다.
npm install mocha chai chai-http sinon mongo-unit --save-dev
보라색 글자는 Mocha 테스트를 위해 새로 생성하는 것을 의미합니다.
파랑색 글자는 Mocha 테스트를 위해 존재하는 파일에 추가 사항이 있는 것을 의미합니다.
터미널에 $ npm test
를 입력해 Mocha 테스트를 실행합니다.
"scripts": {
"test": "mocha \"./test/**/*.spec.js\" --delay"
}
\"./test/**/*.spec.js\"
test
폴더 내의 모든 서브 디렉토리에서 .spec.js
로 끝나는 파일들을 실행함--delay
run()
함수를 사용 가능하게 함scripts
를 설정하고 나중에 $ npm run test
를 입력해주면 자동으로 모든 테스트를 실행합니다.
const mongoUnit = require('mongo-unit');
mongoUnit.start().then(() => {
run() // this line start mocha tests
});
after(() => {
mongoUnit.stop();
console.log('테스트 완료');
process.exit(0);
});
Mongo-unit을 시작한 후 테스트를 실행시키고, 테스트 완료 후 프로그램을 종료해줍니다.
필요시 mongoUnit.load
를 통해 DB의 초기 상태를 설정해주는 것 또한 가능합니다.
Mocha 실행시 --delay
플래그와 함께 실행하여 생성된 run()
콜백 함수를 통해 테스트 시작 전에 비동기 함수를 처리할 수 있게 합니다.
var path = require('path');
var dotenv = require('dotenv');
// 테스트용 env로 설정
dotenv.config({path: path.join(__dirname, '/../.env.test')});
const app = require('../src/app.js');
// 테스트용 App 모듈 Export
module.exports = app;
Chai Request로 보내주기 위한 테스트용 Express 서버 앱을 Export 해줍니다.
const mongoUnit = require('mongo-unit');
let chai = require('chai');
let chaiHttp = require('chai-http');
chai.use(chaiHttp);
// expect, assert, should 중 개인의 취향대로 assertion 진행
var expect = chai.expect;
var assert = chai.assert;
var should = chai.should();
// 테스트용 서버 앱을 불러옴
const app = require('../app.test');
// 테스트 DB의 초기 상태 설정용 데이터 정의
const testData = {
"Collection1": [
{
"_id": "112233445566778899",
"data": "...",
...
},
...
]
};
// Route1 의 GET API
describe('어떤 API 테스트 인지 설명', () => {
// 테스트를 진행하기전 Mongo-unit에 테스트 데이터를 가진 DB를 설정
before(() => mongoUnit.load(testData))
// 테스트
it('it should GET string result', (done) => {
chai.request(app)
.get('/route1/route2') // 라우트 설정
.query({page: 0, limit: 10}) // 파라미터 정의
.end((err, res) => {
expect(res.status).to.equal(200);
const { result } = res.body; // res.body에서 받은 결과 "result"로
result.should.be.a('string'); // result가 string인지 확인
done(); // 테스트 끝 정의
});
});
});
테스트 파일은 이런 식으로 작성하면 됩니다.
초반에 설정해준 scripts로 $ npm run test
를 입력하여 테스트를 실행시킨 결과입니다
작성해둔 테스트 코드를 모두 실행시켜서 그에 대한 결과를 보여줍니다.
그 외에도 에러 코드에 대한 테스트 및 걸리는 시간 등 상세한 테스트 케이스를 작성해 실행시킬 수 있습니다.
잘 작성된 테스트 코드로 테스트를 실행하게 되면 어느 부분에서 에러가 났는지, 어느 부분과 영향이 있는지, 어떤 이유로 에러가 났는지까지 바로 파악이 가능합니다.
사실 API Testing 같은 경우 시도할 수 있는 다양한 방법이 있습니다. 굳이 위의 방법으로 자동화를 하지 않아도 하나하나 Postman으로 테스트하는 방법으로 해도 괜찮습니다.
테스트 자동화를 위해 수많은 테스트 코드를 작성해야 한다는게 할 일이 많아지는 것이기도 하고 시간이 더 걸리게 되는 일이기도 합니다. 그래서 작은 프로젝트를 할 때에나 개발 사항이 그렇게 많지 않은 경우 쉽게 테스트하고 넘길 때도 분명 많습니다. 그게 빠르고 효과적일 때도 있구요.
하지만 테스트 코드를 작성하는게 시간이 조금 더 걸리고 귀찮더라도 잘 작성해서 예상할 수 있는 버그를 확실히 잡는게 낫다고 생각합니다.
유저가 앱을 사용하다가 갑작스럽게 앱이 종료되는 경험만큼 기분 나쁜 게 없잖아요?
제가 저를 위해서, 팀원들을 위해서 "확실" 하게 해야 하는 것 중 하나는 신뢰할 수 있는 개발자가 되는 것이었습니다. 그리고 제가 믿음직한 동료가 된다는 "확신" 을 얻기 위해 API 테스팅을 도구로써 사용해보는 방법을 담아보았습니다.
질문 하나를 던지며 글을 마치려 합니다.
여러분들은 무엇을 "확실" 하게 하고 어떤 "확신" 을 가지고 싶나요?
감사합니다.