JEST를 이용한 SW Test 활용해보기

by skchoi

Contents

소프트웨어 테스트는 필요한 일일까?

개발자로서 작업이 끝나고 그 내용을 위한 테스트 코드를 작성해야 한다는 것은 꽤 귀찮은 일이다.

단기적으로 보면 개발 시간이 연장될 뿐 아니라 그 효용성에 대해서도 의구심이 들기 때문이다. 거기에 더해 어차피 User Action은 전부 고려할 수 없고, 단순한 API 동작 테스트만 하는 게 과연 도움이 될까 하는 회의감도 한 몫 더한다.

지금껏 제대로 동작하는 테스트를 경험해 볼 기회가 없었기에, 위와 같은 의문만 가진 채 테스트 작성을 등한시 해왔었는데, 이번 패스트캠퍼스(FCL)에서 기회가 닿아 테스트를 사용해본 후기를 공유해보려고 한다.

패스트캠퍼스랭귀지(FCL)의 테스트 방법 소개

먼저 패스트캠퍼스랭귀지의 테스트 컨셉과 방법에 대해 소개하고자 한다.

Concept

  • 테스트 툴(Tool)은 Jest를 이용한다.
  • 테스트의 병렬성 및 속도 향상을 위해 Docker 컨테이너(DB)를 복수개 운용하고, DB를 Queue로 요청하여 테스트 별로 acquire(), release() 하도록 설계하였다.
  • 멱등성을 적용하기 위해 Fixture, Snapshot, Rollback 기능을 이용하였다.

멱등성이란? (https://ko.wikipedia.org/wiki/멱등법칙) 멱등성이란 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질을 의미한다. 테스트의 세계에서 멱등성을 적용하려면

초기 상태(Fixture : 고정된 값)가 정의되어야 한다 — 이 초기상태는 테스트와 관련된 DB, 결제, 상품 등을 설정하는 것

초기 상태를 Snapshot을 찍어둔다.

테스트를 실행한다. — 이 때 초기 설정값이 변경 될 수 있다. (DB 내용 변경 등)

하나의 테스트가 완료되면 Snapshot 찍어둔 것을 Rollback하여 다시 초기상태로 되돌린다. (멱등성 유지)

Test Flow

실제 업무에 테스트 적용해 보기

패스트캠퍼스(FCL)에서 내가 맡았던 업무는 결제 관련 모듈과 쿠폰 처리였다.

모두 회사 매출과 재화에 영향이 있다 보니 조심스러운 상황 이었는데, 차근차근 테스트를 만들어 가며 진행했던 것이 도움이 많이 되었다.

Step 1. 테스트 파일 생성

먼저 테스트는 아래와 같은 규칙으로 코드를 생성한다.

  • unittest는 {filename}.unittest.js
  • systemtest는 {filename}.systemtest.js

Step 2. 개발 진행 단계에서 TC 작성

요구사항에 맞는 기능을 개발하면서 개발중인 API를 테스트하는 단순한 TC(Test Case)들을 추가하며 진행한다.

let targets = await orderDomain.findPartnershipCouponTargetList({ partnershipCouponType, now });
expect(targets.length).toBeLessThan(1);
let partnershipCouponList = await couponDomain.getPartnershipCouponList({
partnershipCouponType,
orderId,
partnershipCouponQuantity: orderQuantity,
});
expect(partnershipCouponList.length).toBe(orderQuantity);
let ret = await couponDomain.updatePartnershipCoupon({
userId,
orderId,
orderPackageId,
partnershipCouponNumber,
});
expect(ret).toBe(1);

Step 3. 개발 완료 단계에서 TC 작성

작업이 완료되면, 개발의 본래 의도를 담은 TC와 현 단계에서 예상이 가능한 오류 및 위험요소를 체크할 수 있는 TC를 작성한다.

예를 들어, 요구사항 중에 고객에게 배달이 완료된 주문중에 2주가 지난 목록들을 불러와 쿠폰을 발송하는 기능이 있는데, 개발 의도를 반영한 TC는 아래 플로우를 모두 포함해야 한다.

상품 주문 → 완료 → 주문 완료 부터 2주 지난 시점으로 day 설정 → 쿠폰 리스트 얻어오기 → 얻어온 리스트 메일로 발송

아래는 실제 적용된 예제 코드이다.

test('test sending coupon and update that information', async () => {
const merchantCipher = createMockOrderCipher();
const mockPublisher = newMockPublisher({ mylightDb: db });
const orderDomain = newOrderDomain({
db,
merchantCipher,
publisher: mockPublisher,
});
const couponDomain = newCouponDomain({ db });
let partnershipCouponType = 'wsj';
let now = moment().add(16, 'd').format('YYYY-MM-DD');
// 가주문을 넣고 완료상태로 만드는 것을 Fixture로 정의 하였다.
// 가상 주문의 주문 완료일이 테스트를 실행한 날(today)이기 때문에,
// 그로부터 16일 뒤를 nowTime으로 넘겨주어 테스트가 용이하도록 API를 구성하였다.
const targets = await orderDomain.findPartnershipCouponTargetList({
partnershipCouponType,
now,
});
expect(targets).toBeDefined();
for (const target of targets) {
const { orderId, orderQuantity } = target;
// 쿠폰을 보낼 대상이 있는지 조회한다.
let partnershipCouponList = await couponDomain.getPartnershipCouponList({
partnershipCouponType,
orderId,
partnershipCouponQuantity: orderQuantity,
});
expect(partnershipCouponList.length).toBe(orderQuantity);
}
// 쿠폰을 고객 메일로 전송한다.
await mockPublisher.push({
message: {
messageType: MESSAGE_TYPE_EMAIL,
parameters: {
type: EMAIL_PARAMETER_TYPE_PARTNERSHIP_COUPON,
partnershipCouponType,
now,
},
},
});
for (const target of targets) {
const { orderId, orderQuantity } = target;
// 쿠폰 전송 이후에 쿠폰 대상자를 재조회 할 시, 대상자가 없어야 한다.
const partnershipCouponList = await couponDomain.getPartnershipCouponList({
partnershipCouponType,
orderId,
partnershipCouponQuantity: orderQuantity,
});
expect(partnershipCouponList.length).toBe(0);
}
});

그밖에 예상되는 위험요소는 아래와 같은 것들이 있었는데,

  • 주문 완료 2주전에 쿠폰이 발송되면 안된다.
  • 쿠폰이 제대로 발송 되어야 한다.
  • 발송한 쿠폰이 다시 발송 되면 안된다.
  • 남은 쿠폰이 없는 경우에 대한 처리를 해야한다.
  • 복수개의 패키지를 구매 시 수량 만큼 다른 쿠폰이 지급되어야 한다.

이를 위해 내가 추가한 쿠폰 테스트 목록이다.

  • test(‘check either setting partnership coupon type in order package table is well or not’)
  • test(‘find targets for sending partnership coupon’)
  • test(‘test coupon should not be shipped until two weeks’)
  • test(‘check table about coupon updated state to assigned well’)
  • test(‘test order 2 same package and send 2 different coupon’)
  • test(‘test alert error when there is not coupon’)

Step 4. 작업내용 Push

테스트 작성이 완료되고 작업된 내용을 push 해야 하는데,

husky라는 Hook 기능을 이용하여, 코드 push시 자동으로 모든 test를 실행하게 하였고, 실패시 push가 되지 않도록 구현하였다. (내가 작성한 코드가 다른 기능에 영향을 주지 않는지 등을 더블 체크 할 수 있어 안정화에 도움이 되었다.)

"husky": {
"hooks": {
"pre-push": "yarn test:push"
}
}

소프트웨어 테스트 활용 후기

테스트를 제대로 사용해보면서 느낀점은 내가 생각했던 것 보다 장점이 많았다는 것이다.

모든 케이스를 고려할 수도, 테스트화 시킬수도 없다고 믿었기에 실효성에 대해서 의문이 많았었는데, 이번 기회를 통해 적어도 한가지는 보장 받을 수 있겠다는 믿음이 생겼다.

내가 처음 개발단계에서 의도한 기능이 정상 동작 하는지, 그리고 다른 수정이 그 기능의 의도를 해치지 않는지를 매번 확인할 수 있다는 것이다. 특히 협업을 하면서 다른 사람의 코드와 의도를 모두 파악할 수 없기에 적어도 내 수정분이 다른 테스트를 fail시키지 않는다는 보장은 팀 전체 개발의 효율성 측면에서 강력한 효과를 발휘 한다는 생각이 들었다.

앞으로 테스트에 관련된 노하우나 기술들은 계속 발전 하겠지만, 현재 FCL 개발팀에서 추구하는 방향은 최소한 개발한 모듈의 책임 범위 한에서는 테스트를 작성해야 한다는 것이다. 추후 데이터가 더 쌓이고 빈번히 발생하는 인적 실수 등의 항목들이 유형화될 수 있다면 모듈의 책임을 넘어선 부분까지 테스트의 효용성과 생산성이 올라갈 것이라고 기대하고 있다.

레모네이드 개발팀 기술 블로그입니다. This is an engineering blog from Lemonade Engineering.

레모네이드 개발팀 기술 블로그입니다. This is an engineering blog from Lemonade Engineering.