켄트백의 테스트 주도 개발책을 읽으면서 "어떻게 테스트 할 것 인가?" 에 대해 아래와 같이 먼저 생각해보고
가장 기본적인 전략에 관한 질문에 답해야 한다고 말합니다.
- 테스트 한다는 것은 무엇을 뜻하는가?
- 테스트는 언제 해야 하는가?
- 테스트할 로직을 어떻게 고를 것인가?
- 테스트할 데이터는 어떻게 고를 것인가?
첫번째 질문을 답하는데 테스트 코드와 관련해서 여러 세미나 영상이나 개발 커뮤니티를 찾아보면서 생각했던 건 테스트는 켄트백이 말했던 것 처럼 위험한 리스크를 지루함으로 바꿔주는 것이라고 생각한다.
왜냐하면 테스트를 반복 할 수록 스트레스 받는 정도가 많아지고 더 많은 시간을 투자하면서 점점 지루해지지만 사전에 일어날 수 있는 오류를 발견하고 에러를 줄일 수 있어서 리스크를 줄일 수 있으니까 그렇게 표현한 것 같았다.
테스트를 언제 해야하는가?
테스트 코드를 작성하는데 있어서 양성 피드백 고리와 비교를 했는데 위키에서는 아래와 같이 정의 되어 있었다.
양성 피드백 은 시스템의 출력이 입력량을 늘리는 방향으로 진행되는
피드백이다.
개발을 하는데 스트레스를 받으면 받을 수록 테스트 코드작성을 뜸하게 되고 결국 우린 더 많은 에러를 생성함으로써 더 많은 스트레스를 받게 된다. 결국 시스템의 안정성은 떨어지게 되고 결국 크리티컬한 버그로 이어지는 안좋은 루프를 돌게 되는 것이다. 이러한 순환 루프를 나오려면 어떻게 해야될까?
바로 자동화된 테스트를 도입함으로써 자신이 만든 소프트웨어에 안정성을 보장하고 새로운 기능을 추가하거나 기존 기능을 변경해서 생기는 사이드 이펙트를 사전에 확인할 수 있다. 하지만 궁극적으로는 테스트를 어느 시점에 해야되는지에 대해서는 답변이 되지 않았는데 그건 아래와 그래프와 같이 해당 pay off 라인이 지나기 전에 도입을 해야된다고 생각을 합니다.
이러한 신호는 이제 기능 개발보다 유지보수하는데 시간을 투자할 때, 기능 개발하는 시간보다 기존 코드를 분석하는 시간이 더 오래 걸리기 시작할 때 점점 느껴질 것이라 생각합니다.
격리된 테스트?
비지니스 로직을 설계를 할 때 실무에서는 API, 데이터 베이스, 네트워크, UI 등의 종속되어 코드를 설계하게 됩니다.
켄트백은 키보드와 마우스 이벤트를 기록하여 동작하는 소프트 웨어를 개발을 했었는데 매일 아침 출근하게 되면 책상 위에 테스트 결과가 담긴 종이더미가 있었고 매일매일 다른 양의 이러한 테스트 결과 문서가 쌓이는 악몽을 겪었다고 했다.
개발자도 유저들이 버그리포트를 보내고 우리는 해당 버그리포트의 히스토리를 추적하면서 디버깅을 한다. 하지만 매번 이런식으로 대응하게 된다면 끝도 없는 악몽의 무한 루프에 갖히게 된다.
그래서 켄트백은 두가지 교훈을 공유하였는데 첫번째는 테스트가 빠르게 동작하게 되어 다른 누군가가 발견하기 전에 내가 먼저 발견하는 것과 각각의 테스트를 독립적으로 분리시켜서 문제가 하나면 하나만 실패하고 두개면 두개만 실패를 해야된다는 것이다. 결과적으로 시스템을 응집도는 높고 결합도는 낮은 객체로 구성해야된다고 설명을 한다.
도대체 결합도가 낮고 응집도가 낮은 디자인은 어떻게 해야되고 무슨 방식으로 접근해야되는지 의문이였다.
테스트 목록
첫번째는 테스트 해야할 오퍼레이션을 모두 적어넣고 해당 오퍼레이션은 널을 반환하도록 구현을 한다 마치 아래의 코드 처럼 토큰을 가져오는 메서드지만 정말 아무것도 동작하지 않는 메서드이다.
public String getToken() {
return null;
}
위와 같은 오퍼레이션을 리스트에 적어놓고 작업을 다 끝내기 전에 리팩토링 해야 할 목록을 적는 것 이다. 하지만 이러한 방식으로 진행하게 된다면 리팩토링을 진행할 때 코드에 대한 간섭을 받게 됩니다. 예를 들어 어떠한 변수명으로 바꾸거나 클래스를 분리시켜야 한다고 생각되게 된다면 해당 메서드를 참조하고 있는 코드를 모두 변경을 해야합니다. 그래서 각각 오퍼레이션을 한번에 하나씩 TDD를 진행하는 것이 더 좋다고 설명 합니다.
테스트 우선
테스트는 언제 해야될까?
코드를 작성하고 기능을 완수하게 되면 저는 저의 할 일은 끝났다고 보통 생각합니다. 하지만 해당 코드에서 생각지 못한 버그와 잘못된 설계로 인해 코드 구조를 바꾼일이 많았었습니다.
이렇게 진행하면서 스트레스를 더 많이 받게 되면서 실수가 점점 많아지게 되는 켄트 백이 말한 양성 피드백 고리와 같은현상이 생기게 되는 것입니다.
하지만 켄트 백은 이러한 순환 고리를 끊기 위해 역발상으로 생각하게 되면서 해결하였는데 바로 테스트를 먼저 작성하고 그 후에 로직을 구현하는 것이다. 그렇게 된다면 초기에는 테스트로 인한 스트레스를 받게 되겠지만 점점 버그가 줄면서 악순환의 고리를 끊고 음성적인 주기를 반복하게 되는 것이다.
단언(assert) 우선
단언문은 프로그래밍 안에 참, 거짓을 미리 가정하는 것으로 개발을 진행할 때 먼저 단언문을 미리 작성해놓고 개발을 진행한다면 좀 더 수월한 개발이 될 것 입니다. 아래의 코드처럼 단순한 커뮤니티 신고를 저장하는 메서드인데
@Test
void report() {
CommunityReport communityReport = CommunityReport.builder()
.communityId(166L)
.userId(1L)
.message("신고합니다")
.build();
communityReportService.report(communityReport);
CommunityReport result = communityReportRepository
.findByReportCommunityId(communityReport.getcCmmunityId());
assertThat(result.getMessage()).isEqualTo("신고합니다");
assertThat(result.getUserId()).isEqualTo(1L);
}
단언을 미리 작성해놓고 개발을 하게 된다면 아래와 같은 제임스 뉴커크가 말한 작업 순서 처럼 단순화 시켜 진행할 수 있습니다.
- 시스템은 무슨 일을 해야하고 완료된 시스템은 어떤지 알려주는 이야기를 작성
- 특정 기능을 개발할 때 무슨 일부터 하고 기능이 완료되면 통과해야되는 테스트를 작성
- 태스트를 개발할 때 무슨 일부터 하고 통과해야되는 단언 부터 작성
테스트 데이터
우리가 테스트하기 위해서는 결국 데이터가 필요하고 해당 데이터를 테스트 함으로써 결과를 확인합니다.
테스트 데이터를 사용하는 데에 쉽고 단순한 데이터를 사용해야 합니다. 왜냐하면 코드 리뷰를 진행할 때 동료들이 있고 동료들도 나와 같이 단순하고 쉬운 코드를 이해하기 더 쉬워합니다. 그렇기에 굳이 쉬운 데이터와 어려운 데이터를 사용해도 결과는 동일한데 어려운 데이터를 사용할 필요는 없습니다.
마찬가지로 여러가지의 입력을 테스트해야 될 때 3가지 항목으로 충분히 테스트가 가능한데 10가지로 늘려서 테스트 할 필요가 없습니다. 그리고 순서도 일관적으로 나열하면서 가독성을 높이면 좋다. 1, 2, 3, 4 를 더하는 메서드를 테스트 하는데 헷갈리게 2, 1, 4, 3으로 나열할 필요가 없는 것 처럼 말이다.
테스트 데이터에 대한 대안은 한가지가 더 있는데 그것은 실제 서비스나 비지니스에 사용되는 데이터를 사용하는 것이다. 이러한 데이터를 사용하게 되면 다음과 같은 경우를 테스트하는데 매우 유용합니다
- 실제 이벤트를 통해 수집한 외부 시스템의 결과를 이용하여 실시간 시스템을 테스트하는 경우
- 예전 시스템의 출력과 현재 시스템을 출력을 비교하는 경우 (병렬 테스팅)
- 리팩토링하고 난 뒤 이전 코드와 현재 코드가 정확한 결과값이 나오는지 테스트 하는 경우
명백한 데이터
위와 같은 밈처럼 코드를 작성할 때의 최근의 기억은 뚜렷하지만 시간이 가면 갈수록 코드에 대한 기억력은 점점 흐려집니다.
마찬가지로 테스트 데이터도 명확하게 해당 데이터가 무엇을 뜻하는지 남겨 두지 않으면 수년 뒤 다른 개발자나 심지어 본인이 짠 테스트 데이터인데도 무엇을 테스트하기 위한 데이터인지 알지 못할 수 있습니다. 그렇기에 테스트 데이터도 코드처럼 해당 의도를 명확하게 표현할 수 있도록 작성하는 것이 중요합니다.
Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(49.25, "GBP"), result);
--------------------------------------------------------------------
Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1 - 0.015), "GBP"), result);
해당 챕터를 읽으면서 TDD를 통해 얻어지는 이점과 테스트 코드의 클린 코드와 좋은 디자인이란 무엇이고 어떻게 적용해야되는지 알 수 있는 챕터였습니다.
이전 챕터의 예제 코드를 작성하고 따라할 때에는 TDD에 대해 어떻게 실무에 적용하고 사용해야되는지 갈피를 잡기 어려웠었지만 해당 챕터를 읽고 저의 생각과 비교하여 정리를 하니 TDD에 대해 좀 더 뚜렷해진 기분입니다.
실무에 실제로 적용하는 것은 이제 저의 역량과 노력에 따라 품질이 달렸네요 :)
'도서' 카테고리의 다른 글
클린코드 (함수) (0) | 2022.07.04 |
---|---|
클린 코드 (의미 있는 이름) (0) | 2022.06.26 |
클린 코드에 대하여 (0) | 2022.06.21 |
테스트 주도 개발 패턴 (2) (0) | 2022.05.29 |
TDD 테스트 주도 개발 1부 후기 (0) | 2022.04.17 |