- Published on
클린코드 - 8장. 경계 ~ 9장. 단위 테스트
- Authors
- Name
- Gibo Ryu
- @ryugibo
노마드코더 북클럽 클린코드 TIL 여덟번째
2022년 5월 7일 TIL
오늘 TIL 3줄 요약
- 경계 인터페이스를 노출해서 여기저기 사용하지 말고 가능하면 자체 인터페이스로 감싸서 사용하자.
- 외부 코드를 도입할 때는 학습 테스트를 작성하자.
- 테스트코드도 깨끗하게 관리하자.
오늘 읽은 범위
- 8장. 경계 ~ 9장. 단위 테스트
책에서 기억하고 싶은 내용을 써보세요.
Map
클래스를 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다.Map
을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 말라는 말이다.Map
과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.Map
인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다. (p.181)곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까? 짐 뉴커크Jim Newkirk는 이를 학습 테스트라 부른다. (p.182)
학습 테스트에 드는 비용은 없다. 어쨌든 API를 배워야 하므로······. 오히려 필요한 지식만 확보하는 손쉬운 방법이다. 학습 테스트는 이해도를 높여주는 정확한 실험이다. 학습 테스트는 공짜 이상이다. 투자하는 노력보다 얻는 성과가 더 크다. 패키지 새 버전이 나온다면 학습 테스트를 동려 차이가 있는지 확인한다. (p.184)
경계에 위치하는 코드는 깔끔히 분리한다. 또한 기대치를 정의하는 테스트 케이스도 작성한다. 이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다. 통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다. 자칫하면 오히려 외부 코드에 휘둘리고 만다. 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
Map
에서 봤듯이, 새로운 클래스로 경게를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높이지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다. (p.187)지금 즈음이면 TDD가 실제 코드를 짜기 전에 단위 테스트부터 짜라고 요구한다는 사실을 모르는 사람은 없으리라. 하지만 이 규칙은 빙산의 일각에 불과하다. 다음 세 가지 법칙을 살펴보자.
- 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다.
- 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트를 작성한다.
- 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다.
위 세 가지 규칙을 따르면 개발과 테스트가 대략 30초 주기로 묶인다. (p.190)
테스트 코드는 실제 코드 못지 않게 중요하다. 테스트 코드는 이류 시민이 아니다. 테스트 코드는 사고와 설계와 주의가 필요하다. 실제 코드 못지 않게 깨끗하게 짜야 한다. (p.192)
코드에 유연성, 유지보수성, 재사용성을 제공하는 버팀목이 바로 단위 테스트다. 이유는 단순하다. 테스트 케이스가 있으면 변경이 두렵지 않으니까! 테스트 케이스가 없다면 모든 변경이 잠정적인 버그다. 아키텍처가 아무리 유연하더라도, 설계를 아무리 잘 나눴더라도, 테스트 케이스가 없으면 개발자는 변경을 주저한다. 버그가 숨어들까 두렵기 때문이다. (p.192)
BUILD-OPERATE-CHECK 패턴1이 위와 같은 테스트 구조에 적합하다. 각 테스트는 명확히 세 부분으로 나눠진다. 첫 부분은 테스트 자료를 만든다. 두 번째 부분은 테스트 자료를 조작하며, 세 번째 부분은 조작한 결과가 올바른지 확인한다. (p.196)
- 이것이 이중 표본의 본질이다. 실제 환경에서는 절대로 안 되지만 테스트 환경에서는 전혀 문제없는 방식이 있다. 대개 메모리나 CPU 효율과 관련 있는 경우다. 코드의 깨끗함과는 철저히 무관하다. (p.199)
- 단지
assert
문 개수는 최대한 줄여야 좋다는 생각이다. (p.200) - 이것저것 잡다한 개념을 연속으로 테스트하는 긴 함수는 피한다. (p.201)
- F.I.R.S.T (p.202)
- Fast빠르게: 테스트는 빨라야 한다. 테스트는 빨리 돌아야 한다는 말이다. 테스트가 느리면 자주 돌릴 엄두를 못 낸다. 자주 돌리지 않으면 초반에 문제를 찾아내 고치지 못한다. 코드를 마음껏 정리하지도 못한다. 결국 코드 품질이 망가지기 시작한다.
- Independent독립적으로: 각 테스트는 서로 의존하면 안 된다. 한 테스트가 다음 테스트가 실행될 환경을 준비해서는 안 된다. 각 테스트는 독립적으로 그리고 어떤 순서로 실행해도 괜찮아야 한다. 테스트가 서로에게 의존하면 하나가 실패할 때 나머지도 잇달아 실패하므로 원인을 진단하기 어려워지며 후반 테스트가 찾아내야 할 결함이 숨겨진다.
- Repeatable반복가능하게: 테스트는 어떤 환경에서도 반복 가능해야 한다. 실제 환경, QA 환경, 버스를 타고 집으로 가는 길에 사용하는 (네트워크에 연결되지 않은) 노트북 환경에서도 실행할 수 있어야 한다. 테스트가 돌아가지 않는 환경이 하나라도 있다면 테스트가 실패한 이유를 둘러댈 변명이 생긴다. 게다가 환경이 지원되지 않기에 테스트를 수행하지 못하는 상황에 직면한다.
- Self-Validating자가검증하는: 테스튼느 부울bool 값으로 결과를 내야 한다. 성공 아니면 실패다. 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안 된다. 통과 여부를 보려고 텍스트 파일 두 개를 수작업으로 비교하게 만들어서도 안 된다. 테스트가 스스로 성공과 실패를 가늠하지 않는 다면 판단은 주관적이 되며 지루한 수작업 평가가 필요하게 된다.
- Timely적시에: 테스트는 적시에 작성해야 한다. 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. 실제 코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모른다. 어떤 실제 코드는 테스트하기 너무 어렵다고 판명날지 모른다. 테스트가 불가능하도록 실제 코드를 설계할지도 모른다.
- 사실상 깨끗한 테스트 코드라는 주제는 책 한 권을 할애해도 모자랄 주제다. 테스트 코드는 실제 코드만큼이나 프로젝트 건강에 중요하다. 어쩌면 실제 코드보다 더 중요할지도 모르겠다. 테스트 코드는 실제 코드의 유연성, 유지보수성, 재사용성을 보존하고 강화하기 때문이다. 그러므로 테스트 코드는 지속적으로 깨끗하게 관리하자. 표현력을 높이고 간결하게 정리하자. 테스트 API를 구현해 도메인 특화 언어Domain Specific Language, DSL를 만들자. 그러면 그만큼 테스트 코드를 짜기가 쉬워진다. 테스트 코드가 방치되어 망가지면 실제 코드도 망가진다. 테스트 코드를 깨끗하게 유지하자. (p.203)
오늘 읽은 소감은? 떠오르는 생각을 가볍게 적어보세요
학습 테스트··· 놀랍게도 이전 프로젝트에서 사용하는 SDK의 API를 활용해야 하는 경우가 있어서 작성했던 경험이 있다. 하지만 모든 프로젝트에 참여하는 개발자(기획자, 프로그래머, 아티스트 등등..) 모두가 로컬에서 서버를 띄워서 개발을 하는 환경이었는데, 모두 테스트용 API 서버에 API를 요청하다보면 서로 데이터가 엉키는 경우가 발생할 수 있어서, 기본적으로 비활성되어 있었고, 공통으로 테스트하는 서버에서만 해당 API 기능이 활성화 되어있었다. 그렇기 때문에 MR2을 생성했을 때 돌아가는 단위 테스트에는 동작하지 않았으며, 로컬에서 직접 테스트를 돌리더라도 해당 API 기능을 활성화해야 테스트가 동작했다.
당연한 얘기지만 해당 테스트는 내가 처음 작성한 뒤로 공개적으로3 테스트된 경우는 없으며, 나만 관련 기능을 추가 개발할 때 돌려보는 정도였고 내가 퇴사할 때까지 방치되었다. 아직도 테스트가 남아있을까···?
테스트 코드와는 좀 다른 얘기지만 테스트 코드가 실제 코드만큼 깔끔해야 한다는 내용과 연결되지 않을까 해서 몇자 적어본다. 최근에 UI와 내부 로직을 나눠서 작업하는 경우가 있어서 테스트용 UI를 Slate를 사용해서 코드에 구성했다. 이렇게 구성한 이유는 대충 적어보면 아래와 같다.
- UI 에셋을 프로그래머가 조작하지 않기 때문에 UI 에셋에 의존하면 안된다.: 별도로 테스트용 UI 에셋을 두게되면 해당 에셋이 배포하는 패키지에 딸려가지 않도록 관리해야하며, 해당 에셋들 역시 다른 프로그래머들에게도 존재 여부를 매번 공유해주어야 한다.
- UI에셋에서 사용하는 함수를 임의로 수정해서 문제가 생겼을 경우 패키지까지 진행해야 문제가 발견되는 경우가 있다.: 언리얼 특성상 블루프린트에 발생하는 문제는 해당 에셋을 로드해야 파악이 되기 때문에, 만약에 특정 입력(인벤토리를 여는 등의 조작)을 해야 에셋이 로드되어 테스트 플레이를 종료했을 때 컴파일 에러가 발견되기 때문에 코드에서 임의로 수정한 경우 문제를 파악하기가 어렵다. 반면에 동일한 API를 참조하는 테스트 UI를 코드에 두면 컴파일 시점에 오류를 파악할 수 있다.
요약하면 테스트 UI도 테스트 코드처럼 프로그래머가 관리해야 하기 때문에 UI 에셋이 아닌 코드로 만드는게 좋다라는 생각이다.
언리얼 엔진 테스트 환경을 구성한 사례는 몇차례 봤지만 실제 업무에서는 사용하는 경우를 많이 보지 못한것 같다. 구성하게되면 기본적인 테스트 들은 자동화를 할 수 있어서 좋다고 생각하는데, 이족 업계에서는 별로 관심이 없나보다. 개인 프로젝트에나 시간날 때 적용해봐야 겠다.