테스트 코드를 왜 그리고 어떻게 작성해야 할까?
안녕하세요. 인프랩 백엔드 개발자 하루입니다.
아마 좋은 소프트웨어 설계, 좋은 코드를 위해 테스트 코드를 작성해야 된다는 글을 많이 보셨을 겁니다.
하지만 몇몇 개발자분들은 테스트 코드를 작성하는 것에 공감을 못하기도 하면서 테스트를 막상 작성한다고 하더라도 효과를 보지 못하는 경우들이 있는데요.
그 몇몇 개발자 중 한명이 인프런에 합류하기 전 저이기도 했습니다.
그리고 제가 인프런에 왔을 때 인상 깊었던 것 중 하나는 테스트 코드 존재에 대한 부분이었고 지금 와서 느끼는 부분은 “내가 지금까지 테스트 코드를 작성해야 되는 이유와 잘 작성하는 방법을 몰랐구나.” 라는 것입니다.
이번 포스트에서는 테스트 코드를 왜 작성해야 되고 잘 작성하는 방법에 대해서 제가 조언 받았고 배웠던 내용들을 정리해 보려고 합니다.
테스트 코드를 왜 작성하는 것인가?
첫번째. 디버깅 비용 절감
개발자가 총 개발 소요 시간중 온전히 요구사항에 대한 기능 개발을 하는 시간이 전체의 비중에 어느정도를 차지할까?
위와 같은 물음에 자체 서비스 회사에서 서비스를 운영하고 유지 보수를 해본 경험이 있다면 생각보다 많은 비중을 차지 않는다는 점을 아실 겁니다.
애플리케이션은 항상 내결함성을 가지고 있고 좋은 코드와 설계를 갖추더라도 결함의 존재 자체를 부정할 수 없습니다.
그래서 기능에 대한 버그는 항시 발생하고 그것에 해결하기 위해 개발자는 디버깅을 꾸준히 할 수밖에 없는데요.
저도 이전 회사에서 버그와 이슈들을 리포트 받고 디버깅하고 해결하는데 많은 시간을 소비했습니다.
조금 더 그 시간을 쪼개보자면 문제를 해결하는 시간보다 문제를 찾기 위한 시간이 더 많았습니다.
어느 코드 블록에서 어떤 케이스에서 문제가 발생했는지 알기 너무 힘들었기 때문이었죠.
그렇다면 테스트 코드가 있는 지금의 프로젝트는 어떨까요?
위에서도 언급했듯이 견고한 테스트를 작성하더라도 완벽하게 결함을 없앨 순 없습니다.
하지만 테스트 코드를 통해 오류를 줄일수 있었고 빠르게 대처할 수 있는 내성이 생기는데요.
테스트는 아래의 그림과 같이 범위와 비중에 따라 세 분류(e2e, integration, unit)로 나누어지고 각각의 역할을 가지고 있습니다.
- unit 테스트: 도메인 모델과 비즈니스 로직을 테스트, 작은 단위의 코드 및 알고리즘 테스트
- integration 테스트: 코드의 주요 흐름들을 통합적으로 테스트하며 주요 외부 의존성(ex. 데이터베이스)에 대해서 테스트
- e2e 테스트: 최종 사용자의 흐름에 대한 테스트이며 외부로부터의 요청부터 응답까지 기능이 잘 동작하는지에 대한 테스트
이렇게 구분한 테스트 구간의 경계로 인해 문제가 되는 코드의 범위와 케이스를 빠르게 파악하고 수정할수 있게 되었습니다.
테스트로 인해 결함을 최소화시켜 디버깅에 소모되는 시간을 줄어듬으로써, 서비스 개발자가 비즈니스 개발에 집중하면서 생산성 향상에 도움을 주게 됩니다.
두번째. 코드 변경에 대한 불안감 해소
“버그를 고쳤는데 다른데서 또 터지네…”
위와 같은 상황이라면 여러분은 회귀 버그를 겪고 있는 것입니다.
회귀 버그
이전에 제대로 작동하던 소프트웨어 기능에 문제가 생기는 것을 가리킨다.
일반적으로 회귀 버그는 프로그램 변경 중 뜻하지 않게 발생한다.
-위키 백과
이러한 유형의 버그는 정말 비일비재하게 겪게 되는데요.
왜 그런 것일까요?
사실 이유를 생각해 보면 당연하다고 생각이 됩니다.
애플리케이션에서 기능은 단일 하나의 요소(함수, 객체, 도메인)로 이루어지지 않고 여러 요소들이 서로 상호작용하고 협력하여 만들어집니다.
그렇기 때문에 하나의 기능을 수정할때 구성된 요소가 다른 기능과도 협력하고 있고 영향을 주기 때문에 회귀 버그를 겪게 되는 것입니다.
그렇다면 회귀 버그를 완전히 차단하고 예방할 수 있을까요?
위에서 말했듯이 애플리케이션의 기능은 요소들이 상호작용으로 만들어지기 때문에 사실상 그건 불가능하다고 생각이 듭니다.
회귀 버그는 예방하는 것이 아니라 관리하고 대처해야됩니다.
다시 말해 저희는 회귀 테스트를 해야 합니다.
회귀 테스트는 기능 추가나 오류 수정으로 인해 새롭게 유입되는 오류가 없는지 검증 해줍니다.
개인적으로 제가 테스트가 수 천개 있는 인프런 프로젝트를 진행하면서 느낀 점 중 가장 크게 와닿는 테스트의 이점입니다.
테스트 코드는 그때 당시의 기능을 만들기 위해서만 필요한 코드가 아닙니다.
그 이후에
- 요구사항이 변경되어 기존 코드를 수정하거나
- 더 나은 코드를 위해 리팩토링을 하거나
서비스가 지속 가능하게 발전하기 위해 필요한 코드입니다.
세번째. 더 나은 문서 자료
“아우.. 레거시 코드들 너무 읽기 힘들어..”
저희는 좋은 코드를 작성하기 위해 잘 읽히는 코드, 가독성 좋은 코드를 작성하려고 노력합니다.
하지만 서비스 복잡도가 늘어남에 따라 코드의 양이 많아지고 코드의 복잡도가 높아지게 됩니다.
이러한 복잡한 요구사항이 중첩되어 있는 기능들은 신규 입사자들이 코드를 이해하는데 많은 불편함을 겪게 합니다.
그래서 저희는 그들의 편의를 위해 따로 문서화를 해두지만 그 문서를 쉽게 방치합니다. 이것이 문서화의 가장 고질적인 문제점입니다.
이 문제점이 발생하는 이유가 무엇일까요?
저의 생각은 개발자가 문서와 코드를 같은 유지 보수 대상과 범주로 가져가는 게 어렵기 때문이라고 생각합니다.
두 대상을 분리하고 물리적인 거리감이라는 이유로 코드의 수정에 맞춰 문서의 최신화가 잘 이루어지지 않는 경우들이 많기 때문이죠.
그래서 소스 코드 외의 별도의 문서자료들은 관리 대상으로서 쉽게 제외되기 때문에 신뢰하기 어려워집니다.
저 또한 문서화를 하면서 나중에는 새로 만드는 것보다 기존 문서들을 유지 보수하는 것이 더 귀찮은 일이라고 생각했었습니다.
하지만 테스트가 귀찮은 일이라고 생각했던 문서의 역할을 해줄 수 있다는 것을 알게 되었습니다.
“문서화할 내용을 나와 가장 가까운 IDE, 코드에 배치하는 건 어떨까?”
아래와 같이 테스트를 작성할 때 가장 먼저 명세를 작성하면서 코드의 실제 동작을 기술함으로써 이 코드가 어떤 역할을 가졌는지 이해하는데 도와줍니다.
기존에 중복된 메일이 있는 경우 회원가입을 할 수 없습니다.
문서화 테스트는 기능과 코드를 이해하는 데 도움을 줍니다.
네번째. 좋은 코드는 테스트하기 쉽다.
좋은 코드는 “변경하기 쉬운”이라는 형용사를 가지고 있는데요.
이 의미는 약한 결합도을 가지고 있는 코드를 뜻하며 반대로 강결합이 되어있는 코드는 유지비용이 증감되어 저품질코드로 분류되어집니다.
그렇다면 강결합으로 이루어진 코드를 테스트하기 쉬울까요?
당연히 매우 어렵습니다.
외부의 영향을 받거나 내부적으로 의존성을 가지고 있는 코드는 변경에 유연하게 대응하지 못하고 재사용하기 어려운 코드들입니다.
그렇기 때문에 테스트를 작성하기 어려운 코드가 만들어지는 것입니다.
그렇다고 테스트하기 쉬운 코드가 모두 좋은 코드가 되는 것은 아닙니다. 하지만 저희는 테스트를 작성하면서 하나의 좋은 코드의 지표를 아래와 같이 세울수 있게 됩니다.
“만약 내가 작성한 코드가 테스트하기 어려운 코드라면 냄새나는 코드일 가능성이 높아.”
다섯번째. 테스트 자동화
“신이시여, 이 프로젝트를 구원해주소서.”
개발자의 기도메타는 본인이 작성한 코드가 실제 운영 환경에 배포됐을때 불안감에 휩싸여 기도를 시작합니다.
“내가 작성한 코드가 정상적으로 동작할까?”
“내가 작성한 코드로 인해 다른 부분에서 오류가 생기지 않을까?”
이런 오만가지 생각이 들면서 개발자의 불안감을 점점 커져갑니다.
하지만 자동화 테스트가 있는 순간부터 우리는 CI에서 우리가 작성한 테스트 코드로 버그가 배포되는 것을 방지할 수 있게됩니다.
앞서 이야기한 변경에 대한 내성과 회귀 버그를 지속적인 코드 병합 단계에서 확인할 수 있으므로 소프트웨어의 품질 또한 높여줍니다.
즉, 테스트 코드가 존재함으로써 안정감 있는 프로젝트 아니 안정감이 있는 우리의 모습을 볼 수 있게 됩니다.
테스트 코드를 잘 작성하는 방법 with Jest
지금까지 “테스트 코드를 왜 작성하는 것인가?”에 대해서 5가지 이유를 알아보았습니다.
테스트 코드를 작성해야 되는 이유를 알았다면 이제는 테스트 코드를 어떻게 작성해야 하는지에 대해서 알아보겠습니다.
현재 인프런에서 typescript
와jest
프레임워크로 테스트 코드를 작성하고 있기 때문에 예제 코드는 해당 언어와 프레임워크로 사용하였습니다.
첫번째. 테스트 코드는 DRY 보다는 DAMP 하게 작성하라.
개발 원칙 중 우리는 DRY(Don’t Repeat Yourself) 원칙을 많이 들어보셨을 겁니다.
개발자인 저희는 중복 코드를 싫어해 중복 코드가 보이면 그것을 어떻게든 없애려고 하는 경향이 있는데요.
그렇다면 중복을 줄이기 전에 DAMP하게 테스트 코드를 작성하는 것을 고려해봐야합니다.
DAMP 원칙은 Descriptive and Meaningful Phrases의 약자로 의미있고 설명적인 구문을 사용하라는 원칙입니다.
이 원칙을 이해하기 위해 아래의 예제 코드를 살펴보겠습니다.
describe('지원자', () => {
it('지원자를 최종합격시킨다.', () => {
const name = 'haru';
const status = JobApplicantStatus.IN_PROGRESS;
const jobApplicant = JobApplicant.create(name, status);
jobApplicant.pass();
expect(jobApplicant.status).toBe(JobApplicantStatus.PASS);
});
it('지원서를 불합격시킨다.', () => {
const name = 'haru';
const status = JobApplicantStatus.IN_PROGRESS;
const jobApplicant = JobApplicant.create(name, status);
jobApplicant.fail();
expect(jobApplicant.status).toBe(JobApplicantStatus.FAIL);
});
});
위 테스트 코드는 지원자에 대한 최종합격과 불합격 기능을 테스트하는 코드인데요.
작성된 테스트 코드를 보시고 약간 불편함을 느낄수 있습니다.
왜냐하면 위 두 테스트 코드에서 아래의 3줄 코드가 중복으로 사용되고 있기 때문입니다.
const name = 'haru';
const status = JobApplicantStatus.IN_PROGRESS;
const jobApplicant = JobApplicant.create(name, status);
그래서 이 코드의 중복을 없애기 위해 테스트 각각의 실행 전 수행되는 beforeEach
(setup 메소드)에 아래와 같이 작성하였습니다.
describe('지원자', () => {
let jobApplicant: JobApplicant;
beforeEach(() => {
const name = 'haru';
const status = JobApplicantStatus.IN_PROGRESS;
jobApplicant = JobApplicant.create(name, status);
});
it('지원자를 최종합격시킨다.', () => {
jobApplicant.pass();
expect(jobApplicant.status).toBe(JobApplicantStatus.PASS);
});
it('지원서를 불합격시킨다.', () => {
jobApplicant.fail();
expect(jobApplicant.status).toBe(JobApplicantStatus.FAIL);
});
});
위와 같이 중복을 제거해 잘 작성된 테스트 코드라고 생각할수 있지만,
만약 “지원서를 불합격 상태일때 보관할수 있다.”라는 추가기능이 들어가 테스트 코드를 작성하게된다면 아래와 같이 작성이 될텐데요.
// ...
it('지원서를 불합격 상태일때 보관할수 있다.', () => {
jobApplicant.putStorage();
expect(jobApplicant.isStorage).toBe(true);
});
이 테스트 코드를 추가하고 실행했더니 테스트가 실패하게됩니다.
왜냐하면 우리는 불합격 상태인 지원서를 보관하기위해 putStorage
메소드를 호출하였지만,
beforeEach
메소드에서 지원자 상태를 불합격(FAIL) 상태가 아닌 진행중(IN_PROGRESS)으로 초기화를 하였기 때문에 실패된것입니다.
이 과정에서 실패의 원인을 파악하면서 다시 성공시키위해 beforeEach
메소드를 파악하면서 수정해야되는 상황입니다.
즉 테스트 코드를 작성하는 사람도 테스트 코드를 파악하는 사람도 어려운 테스트가 되었습니다.
이유가 무엇일까요?
바로 테스트 코드의 중복을 줄이면서 테스트 간의 결합도를 높였기 때문입니다.
테스트를 위해 status
를 전역 변수인 공유 상태로 둠으로써 테스트의 수정이 어려워지고 테스트간의 영향을 주는 코드가 되었습니다.
또한, 이 테스트는 이전보다 가독성이 떨어졌습니다.
저희는 테스트를 추가하거나 수정할때마다 항상 beforeEach
메소드를 파악해야되고,
그래서 개별 테스트 코드안을 보고는 이제 코드 전체를 파악할 수 없게 되었습니다.
이러한 문제를 해결하기 위해 테스트 코드의 리팩토링은 중복을 줄이는 것이 아니라 더 서술적이고 의미있게 작성하는 방향으로 이루어져야 합니다.
오히려 무작정 코드의 중복을 줄이면 테스트 간의 격리성이 떨어지고 테스트 본래의 의도가 점점 모호해져 파악을 제대로 할수 없게되어지기 때문입니다.
Better:
- 테스트의 중복을 줄이는 것이 아니라 더 서술적이고 의미있게 작성하는 방향으로 리팩터링을 해야한다.
- 테스트는 서로 독립적이고 격리되어야 하기때문에 테스트 수정에 다른 테스트가 영향을 받지 않도록 해야한다.
- DAMP 원칙을 지키면서 중복을 줄이는 방안으로는 테스트 픽스쳐 함수나 클래스등을 사용할 수 있다.
describe('지원자', () => {
it('지원자를 최종합격시킨다.', () => {
const jobApplicant = JobApplicantFixture.create(JobApplicantStatus.IN_PROGRESS);
jobApplicant.pass();
expect(jobApplicant.status).toBe(JobApplicantStatus.PASS);
});
it('지원서를 불합격시킨다.', () => {
const jobApplicant = JobApplicantFixture.create(JobApplicantStatus.IN_PROGRESS);
jobApplicant.fail();
expect(jobApplicant.status).toBe(JobApplicantStatus.FAIL);
});
it('지원서를 불합격 상태일때 보관할수 있다.', () => {
const jobApplicant = JobApplicantFixture.create(JobApplicantStatus.FAIL);
jobApplicant.putStorage();
expect(jobApplicant.isStorage).toBe(true);
});
});
class JobApplicantFixture {
static create(status: JobApplicantStatus, name = 'haru') {
return JobApplicant.create(name, status);
}
}
두번째. 테스트는 구현이 아닌 결과를 검증하도록 한다.
테스트 코드를 작성할때 빠른 테스트를 위해 모의 객체를 사용할때가 있습니다.
모의 객체로 mock, spy, stub등을 사용하는 경우 테스트 대상의 구현을 알아야만 테스트를 작성할수 있는데요.
그런데 이 부분을 남용하거나 잘못 사용하여 테스트를 작성하는 경우가 종종 있습니다.
예를 들어 아래와 같이 지원자를 합격 상태로 변경하기 전에 지원자가 현재 불합격이거나 취소상태가 아닌지에 대한 조건이 있다고 가정해 봅시다.
export class JobApplicant {
updatePass() {
this.validateIsNotCancel();
this.validateIsNotFail();
// 유효성 검증 통과후 상태 변경
}
validateIsNotFail(): void {
// 검증
}
validateIsNotCancel(): void {
// 검증
}
}
위 기능에 해당하는 테스트 코드를 jest
의 spyOn
메소드를 통해 작성해보면 아래와 같습니다.
jest.spyOn 메소드는 특정 객체의 메소드를 모의 객체로 대체할 수 있습니다.
객체와 객체의 method명을 인자로 받으며 모의 객체를 반환합니다.
it('지원서를 합격시킨다.', () => {
const jobApplicant = new JobApplicant();
jest
.spyOn(jobApplicant, 'validateIsNotCancel')
.mockReturnValue(undefined);
jest
.spyOn(jobApplicant, 'validateIsNotFail')
.mockReturnValue(undefined);
jobApplicant.updatePass();
expect(jobApplicant.validateIsNotCancel).toBeCalledTimes(1); // 호출되었는지 검증
expect(jobApplicant.validateIsNotFail).toBeCalledTimes(1);
expect(jobApplicant.status).toBe(JobApplicantStatus.PASS);
});
위 작성된 테스트를 보고 상태 변경에 대한 검증 뿐만 아니라 validateIsNotCancel
와 validateIsNotFail
메소드가 호출되었는지 검증을 해서 더 완벽한 테스트라고 생각할 수 있습니다.
하지만 이 테스트는 깨지기 쉬운 테스트이면서 좋은 테스트가 아닙니다.
만약 validateIsNotCancel
메소드의 네이밍이 변경되거나 validateIsNotFail
메소드가 삭제가 되면 이 테스트는 깨지게 됩니다.
즉 구현에 의존적이며 테스트의 목적이 구현에 맞춰져 있는 테스트입니다.
내부 구현이나 비공개(private) 메소드들은 언제든지 바뀔 여지가 있는 코드이기 때문에 정보의 은닉을 위해 숨기는데 다시 정보를 꺼내여 테스트하는 것은 좋지 않습니다.
테스트 코드는 내부 구현보다는 실행 결과에 집중하는 것이 리팩토링 내성을 높일수 있습니다.
또 하나 이 코드에 아쉬운 점은 바로 호출되었는지에 대한 검증입니다.
expect(jobApplicant.validateIsNotCancel).toBeCalledTimes(1); // 호출되었는지 검증
expect(jobApplicant.validateIsNotFail).toBeCalledTimes(1);
이 검증 자체는 의미가 없는 검증과도 마찬가지입니다.
이 테스트의 검증 목적은 합격 상태 변경에 대한 검증으로도 충분합니다.
오히려 이 테스트가 검증하고 싶은 목적을 흐리게 만들뿐이며 테스트 코드를 이해하는데 방해가 됩니다.
지나친 모의 객체 남용과 내부 구현 검증은 오히려 리팩터링의 내성을 떨어뜨리고 유지보수를 더 어렵게 만듭니다.
우리는 어떻게 라는 내부 구현 검증보다는 무엇을 이라는 결과 검증에 집중하여 테스트 코드를 작성해야 합니다.
Better:
- 테스트 코드는 내부 구현이 아닌 실행 결과에 집중해야 한다.
- 의미있는 테스트, 검증을 하자.
it('지원서를 합격시킨다.', () => {
const jobApplicant = new JobApplicant();
jobApplicant.updatePass();
expect(jobApplicant.status).toBe(JobApplicantStatus.PASS);
});
it.each([
['취소', JobApplicantStatus.CANCEL],
['불합격', JobApplicantStatus.FAIL],
])(
'%s 상태의 지원서는 합격시킬수 없습니다',
(_, status) => {
const jobApplicant = new JobApplicant();
jobApplicant.status = status;
expect(() => jobApplicant.updatePass()).toThrowError();
},
);
세번째. 읽기 좋은 테스트를 작성하라.
테스트 코드는 메인, 제품 코드을 위한 코드라 할지라도 우리의 책임이고 관리 대상입니다. 테스트 코드도 가독성이 좋아야하고 불명확한 테스트 코드는 읽는 행위도 유지보수 하는 행위도 어렵게 만듭니다.
만약 실패한 테스트 코드가 아래와 같은 코드라고 가정해보겠습니다.
it('포스트 글의 좋아요수를 업데이트한다.', () => {
const a = User.create('test', 1);
const b = User.create('test', 2);
const c = Post.create('test', a, 3);
c.updateLikeCount(300);
expect(c.like).toBe(300);
})
테스트 코드를 파악하기 위해 우리는 위 코드를 보면서 많은 노력을 기울어야 할지도 모릅니다.
심지어 User.create
, Post.create
, c.updateLikeCount
메소드의 구현부를 봐야할수도 있습니다.
이처럼 무엇을 고쳐야 하는지 파악하기 어려운 테스트는 잘 작성된 테스트가 아닙니다.
좋은 테스트 코드는 읽는 사람 입장에서 이 테스트를 이해하는데 필요한 모든 정보를, 테스트 케이스 본문에 담고 있는 테스트를 말합니다.
그리고 이와 동시에 관련없는 정보는 담지 말아야 합니다.
또한 테스트 코드의 가독성을 높이는 방법중 하나는 테스트의 구조를 잘 잡는 것인데요.
테스트의 구조는 준비, 실행, 검증 3개의 구절로 나뉘어질 수 있습니다.
각각의 구절을 AAA 패턴(Arrange-Act-Assert 주석으로 구분)이나 GWT 패턴(given-when-then 행위 주석으로 구분)으로 구간을 나누어 작성하는것이 좋습니다.
(물론 구조를 위한 주석을 사용하지 않고 각 영역에 맞게 테스트 함수 내 코드를 구분해서 작성할수 있다면 그 방안을 사용하는 것이 좋습니다.)
그리고 만약 테스트안에 너무 많은 양의 코드가 존재한다면 이부분을 좀더 코드 재사용의 목적으로 모듈화(테스트 팩토리, 빌더, 핼퍼 메소드)해두면 쉽게 읽힐수 있게됩니다.
Better:
- 테스트 코드도 코드이므로 읽기 쉬운 코드를 작성하자.
- 무엇이 잘못되어 실패했는지, 어떻게 고쳐야 하는지를 파악하기 어려운 테스트를 작성하지 말자.
- 테스트 구조를 나누는 패턴과 테스트 코드를 모듈화하는 방법을 통해 테스트 코드를 읽기 쉽게 작성하자.
it('포스트 글의 좋아요수를 업데이트한다.', () => {
// given
const givenLikeCount = 3;
const post = Post.create('title', createUser(), givenLikeCount);
// when
const updateLikeCount = 300;
post.updateLikeCount(updateLikeCount);
// then
expect(post.like).toBe(givenLikeCount + updateLikeCount);
})
function createUser(name = 'name', age = 1) {
return User.create(name, age);
}
네번째. 테스트 명세에 비즈니스 행위를 담도록 한다.
마지막으로 테스트 명을 작성할 때도 유의할 점이 있습니다.
테스트명은 퍼블릭 인터페이스와 마찬가지로 명확하게 의도가 들어나도록 작성이 되어야합니다.
테스트 코드 작성 이유중 세번째 이유. 더 나은 문서 자료
에 대해서 이야기를 한적이 있습니다.
좋은 테스트 코드는 우리가 작성한 코드의 문서이기에 명확한 의도가 당긴 명세가 되어야 합니다.
여기서 명확하게 의도가 드러나는 이름은 개발자의 용어가 아닌 비즈니스 행위를 담은 비개발자가 읽을수 있게 설명되어야 합니다.
Bad:
it('관리자를 생성한후 관리자 정보를 확인한다.')
Better:
it('관리자 정보로 가입한다')
이렇게 잘 작성된 이름은 다음과 같은 효과를 가지고 옵니다.
- 테스트 코드와 실행 로그를 확인하지 않아도, 동작을 유추할 수 있으며, 요구사항이 빠르게 공유가 될수 있다.
- 영향 범위를 빠르게 파악할 수 있으며 유추를 통하여 빠른 복구를 할 수 있다.
마무리
테스트에 대한 제 생각을 장황하게 표현한거 같아 제가 전달하려는 메시지가 잘 전달되었는지 모르겠습니다.
테스트의 중요성을 알고 있지만, 쉽게 도입을 망설이시는 분들도 계실 거라는 생각이 듭니다.
또한 테스트를 도입하는 것은 프로젝트 성격에 따라 ROI 측면에서 어려울 수 있다고 생각합니다.
이 글을 통해 작게나마 테스트 코드는 “지속 가능한 서비스, 프로젝트”를 위해 필수적인 요소중 하나라는 점을 알고 가셨으면 좋겠습니다.
마지막으로 부족한 글을 읽어주셔서 감사합니다.