스터디를 진행하며 굉장히 개인적인 생각과 정리한 내용입니다.
1장
- 단위 테스트는 더 나은 설계로 이어진다 → 단위 테스트의 주요 목표 X 지속 가능한 소프트웨어를 위한 기반을 만드는 것
- 코드가 늘어나면서 발생하는 복잡도를 선형적으로 만들어줌
- 소프트웨어 엔트로피 비유가 인상깊음
- 무언가 변경하면 무질서도 증가함
- 좋은 테스트 코드 ~~ 제품에 대한 이해도와 비례하지 않을까 ?
- 좋은 테스트 코드는 작업 소요 시간이 잘못된 테스트, 테스트를 짜지 않는 것 보다 오래걸림 ⇒ 어느정도 투자할 것인가 ?
- 테스트도 코드 작성의 일부, 코드가 많아지면 잠재적인 버그가 생김 따라서 소프트웨어 원칙들이 적용됨.
- 커버리지는 요구사항이 될 수 없음.
⇒ 단위 테스트를 잘 작성하는 것도 중요하지만, 단위 테스트를 작성하도록 권장하는 분위기가 시작 아닐까 ?
그래야 지속적이고, 장기적으로 할 수 있음. 이 과정에서 좋은 테스트를 찾아나감.
⇒ PRD 조건들 → 테스트 단위 ?
2장
단위 테스트 두 분파 고전파와 런던파 당신(여러분)이 바라보는 관점은 ?
- 내가 런던 사람이였다니-!
- 저자는 고전(Classicist)을 선호
- 관점 차이 : 격리 문제를 어떤식으로 다루느냐?(의존성 취급에 대한 방법)
테스트 대역 : 복잡성을 줄이고 테스트를 용이하게 하는 목적을 가짐 스턴트 대역에서 유래.
목은 테스트 대역의 부분 집합
런던파의 접근
- 객체 간의 상호 작용에 관심사가 있음
→ 대역은 우리가 원하는대로 넣어주기 때문에 테스트가 실패한다면 해당 테스트 대상이 문제가 될 수 밖에 없음
테스트를 위한 의존성을 모두 다루는 것은 어렵기 때문에 필요한 부분만 분할 가능
→ (주관적인 생각) 전체 의존성 그래프를 다시 만드는 것은 너무 어려울 것 같음.
→ (주관적인 생각) 다만 의존성 그래프가 어렵다는 것은 하나의 테스트가 너무 방대한 양을 다루기 때문이 아닐까? 싶은 생각도 듬.
- 테스트르 위해 복잡한 전체 그래프를 설정하기 보단, 먼저 복잡한 그래프를 갖지 않는데 집중하자.
고전파는 공유 의존성만 테스트 대역으로 대체해서 사용하는 경향을 가짐.
각각의 테스트에서 어떤 것을 공유하고 어떤 것을 공유하지 않을 것인지 고민이 필요함
- 싱글턴 객체를 매번 생성할 수 있지만, 매번 새로운 파일 시스템이나, 데이터 베이스를 만드는 것은 쉽지 않음 이 경우 테스트간 공유하거나, 대역(공유 의존성)으로 대체(부가 효과로 테스트 실행 속도도 상승)
SUT 에서 협력자를 격리 ⇒ 런던파 / 단위 테스트 끼리 격리 ⇒ 고전파
테스트의 단위를 검증하기 보단 비즈니스 담당자(개발자)가 유용하다고 인식할 수 있는 것에 집중해야함
- 책의 예시는
- “우리집 강아지를 부르면 나에게 온다” (O)
- “우리집 강아지를 부르면 앞다리가 움직이고, 어쩌고 저쩌고.. 움직인다..” (X)
고전 스타일로 작성된 대부분의 테스트는 런던파 지지자들에게는 통합 테스트 처럼 느껴진다.
통합 테스트는 단위 테스트의 필요조건(단일 동작, 빠르게 수행, 다른 테스트와 분리)을 하나 이상 충족하지 못하는 테스트라 볼 수 있음
3장
- 3A(Arrange Action Assert) 패턴을 기본 구조로 설명하고 있음
- 읽는 구조 외에는 GWT 와 특별한 차이점은 없음
- 제품 코드가 이미 작성된 상태에서 일관성 있고, 직관적이게 작성할 수 있음 이는 곧 유지보수 비용의 절감과 이어짐
- 지양해야 하는 포인트
- 여러개의 준비 실행 검증 구절이 있다면 각각의 테스트로 분리 필요(해당 케이스는 통합 테스트에 가깝다고 책에서 표현) → 테스트도 지속적인 리팩토링이 필요(?)
- 되도록 if 문을 피하자 → 테스트 작성 경험이 아주 많은 편은 아니지만 실제로 if 문을 작성해본 기억이 없음
- 위와 비슷한 이유로 너무 많은 것을 검증한다는 표시
- 준비 구절(SUT)이 너무 크면 별도의 팩토리나 private 로 분리하자(더 나아가서 재활용되면 더욱 좋을듯)
- 오브젝터 마더 & 테스트 데이터 빌더 등이 존재
- 오브젝터 마더 : 객체 생성 로직을 중앙 집중화 함
- 테스트 데이터 빌더 : 객체 생성 로직을 빌더 패턴을 이용해 해결
- 오브젝터 마더 & 테스트 데이터 빌더 등이 존재
- 실행 구절은 보통 한줄로 구성됨
- 의존성과 SUT(검증의 대상) 구분을 위해 객체 이름을 sut 로 명명 하자 & 테스트가 복잡하지 않다면, 준비 실행 검증에 대한 설명은 주석 보다는 줄내림으로 구분하자
- 테스트 수정시, 다른 테스트에 영향을 주는 것은 지양
- 테스트 클래스 내에 멤버 변수를 둔다거나, 생성자 등을 통한 초기화는 각각 테스트에 영향을 줄 수 있음(또한 초기 설정된 값을 보기위해서는 항상 생성자로 이동해야하는 불편함이 있어서 가독성이 떨어짐) → 위에서 이야기 한 것 처럼 별도의 팩토리나 private 함수를 통해 인자로 받으면 해결 / 당연히 예외도 있음 (특정 상태의 DB 인스턴스 생성 등)
- 테스트 이름짓기
- 유명하지만 도움이 되지 않는 방법 중 하나
- [테스트 대상 메서드][시나리오][예상 결과]
- 표현력 있고 읽기 쉬운 일반적인 영어 구문으로 쓰자.
- 유명하지만 도움이 되지 않는 방법 중 하나
- 동작이 복잡하면 parameterized test 는 지양하자
- 테스트 이름으로만 풀어내기 어려움이 있으니, 고유의 테스트로 각각 나타내자
4장
핵심
- 가치 있는 테스트를 작성하려면, 가치 있는 테스트가 무엇인지 판단할 수 있어야 한다.
- 그래야 다양한 도메인에서 응용이 가능하다.
좋은 단위 테스트 요소
- regression 방지
- 기능을 추가하고, 이전 기능이 의도한 대로 동작하지 않는 것을 방지
- Why? 배포전에 이미 테스트를 통해 기능이 동작하지 않는 다는 것을 알 수 있고, 이를 고칠 기회를 얻게 됨
- 따라서 핵심 로직에 테스트 코드가 많으면 많을수록 좋다고 이야기하고 있음
- 기능을 추가하고, 이전 기능이 의도한 대로 동작하지 않는 것을 방지
- 리팩터링 내성
- 리팩토링 할 때, 동작이 변경되지 않는다면 테스트 결과에 영향을 주면 안됨 (3장에서 이야기한 것 처럼 코드를 검증하는 것이 아니라, 동작을 검증해야함)
- Why? 동작이 변경되지 않는데, 리팩토링 했다고 테스트가 깨지면 제품에는 문제가 없는 것이고, 테스트만 문제가 발생하는 것으로 볼 수 있음(False-Positive)
- 타당한 이유 없이 테스트가 실패하면, 테스트 코드를 대응하는 의지가 희석됨
- 결과적으로 테스트에 대한 신뢰가 서서히 떨어지고, 1번의 regression 실패를 방지하려고 코드 변경을 최소한으로만 하려고 하게 됨(적절한 리팩토링의 시점을 놓치게 됨)
- Why? 동작이 변경되지 않는데, 리팩토링 했다고 테스트가 깨지면 제품에는 문제가 없는 것이고, 테스트만 문제가 발생하는 것으로 볼 수 있음(False-Positive)
- 리팩토링 할 때, 동작이 변경되지 않는다면 테스트 결과에 영향을 주면 안됨 (3장에서 이야기한 것 처럼 코드를 검증하는 것이 아니라, 동작을 검증해야함)
드러낸 문제를 해결하는 것 보다, 숨어 있는 문제를 찾고 해결하는 과정이 훨씬 어려움
좋은 테스트 코드
- SUT 와 느슨하게 결합하여 영향을 받지 않으면서, 동작의 **결과(의미 있는 유일한 결과, 가치 있는 값)**를 검증을 지향
- e.g. MessageRender 의 sub Render(SUT) 에 값이 할당되어 있는지 검증보다, 최종 결과인 HTML Plain Text 를 검증하자 (HTML 출력이 변경되지 않는 한 실패하지 않음)
테스트 정확도가 높다 == 발견된 버그 수 / 허위 경보 발생 수 → 객관적으로 관리하는 곳이 있을까 ? 허위 경보 발생수를 어떻게 수치화 하지 ?
- confusion matrix 에 대한 주관적인 생각
- false positive, false negative 가 보통 문제가 됨.
- 암에 걸리지 않은 사람에게 암에 걸렸다고 하는 것(false positive)
- 암환자에게 암에 걸렸다고 하지 않는 것 (false negative) ⇒ 최악의 케이스
- false positive, false negative 중에도 최악을 잘 가려내야할듯.
- 회귀 방지와 리팩터링 내성중에 항상 최악을 대비 해야한다는 의미로 해석됨.
- 최악의 상황은 제품에 문제가 생기는 회귀 방지라고 판단
- 회귀 방지와 리팩터링 내성중에 항상 최악을 대비 해야한다는 의미로 해석됨.
- false positive, false negative 가 보통 문제가 됨.
- 대부분 적용 될 수 있는 개념
- 구매 검증이라면
- 사용자가 쿠폰을 구매 했는지/구매하지 않았는지
- 구매한 쿠폰을 제공해줬는지 제공하지 않았는지
- 구매하지 않은 사용자에게 쿠폰을 제공하는 것도 문제지만, 구매한 사용자에게 쿠폰을 제공하지 않는다고 판단하는 것이 더 최악의 케이스
- 체크인도 응용 가능
- 실제로 유저가 체크인 하려는 장소에 있는지 없는지
- 우리 시스템이 체크인 시켰는지 / 시키지 않았는지
- 실제로 체크인 하려는 장소로 이동했는데, 체크인 시켜주지 않는다면 최악의 케이스
- 구매 검증이라면
리팩터링 내성은 장기 프로젝트에서 의미가 있기 때문에 초반에는 비교적 주요하지 않다고 생각 되는 경향이 있음 (해당 책에서는 굉장히 중요하다고 이야기 하고 이는 곳 저자가 장기 프로젝트를 기준으로 이야기하고 있다고 생각됨)
이상적인 테스트는 리팩터링 내성, 회귀 방지, 빠른 피드백에서 적절한 조화를 가져야하고, 저자는 리팩터링 내성은 항상 챙겨야 하는 요소이고 회귀 방지와 빠른 피드백에서 trade-off 하기를 권장
회귀 방지 (엔두 투 엔드 테스트에 가까움 - 비싸고 오래 걸림)
빠른 피드백(유닛 테스트에 가까움 - 빠르지만, 제품에 대한 검증 정확도는 떨어질 수 있음)
그 중간에 통합 테스트도 존재한다.
테스트를 분석할 때는 화이트 박스로 이용하고, 작성할 때는 내부 구조를 전혀 모르는 것 처럼(블랙박스) 작성하자
5장
- 테스트 대역?
- 비운용성 가짜 의존성을 설명하는 포괄적인 용어(제품에 사용되지 않음)
- 주요 목표 → 테스트를 편리라게 하는 것
- 크게 보면 2가지 유형
- 목 (+ 스파이)
- 외부(서비스)로 나가는 행위(상호작용)
- 예시) SMTP 서버에 사이드 이펙트를 발생시키는 이메일 발송
- 스파이는 수동 작성 목은 프레임 워크의 도움을 받아 생성
- SUT 관련 의존성 간의 상호작용 모방 + 검사
- 다양한 의미로 해석 되는편..
- 테스트 대역의 한 부분이기도 하지만, 목 라이브러리를 지칭 하기도 함
- 당연히 후자는 목을 만드는데 도움을 주지만, 자체로는 목이라 부를 수 없음
- 테스트 대역의 한 부분이기도 하지만, 목 라이브러리를 지칭 하기도 함
- 외부(서비스)로 나가는 행위(상호작용)
- 스텁 (+ 더미, 페이크)
- 내부로 들어오는 행위(상호작용)
- 예시) 데이터 베이스에서 데이터를 검색하는 행위
- 더미는 null, 단순 하드코딩 값 - 메서드 시그니처를 맞추기 위한 값 → 결과에 영향을 직접 주지는 않음
- 스텁은 시나리오마다 원하는 값을 반환할 수 있는 의존성을 갖춘 객체
- 페이크는 동작하는 제품코드는 이미 존재하지만 이를 구현하는데 어려움이 있는 경우 사용 - 목 프레임워크가 필요하지 않고, 가볍게 사용 가능
- SUT 관련 의존성 간의 상호작용을 모방
- 내부로 들어오는 행위(상호작용)
- 이미지 보고 이해하기 좋은 자료
- 도구로서의 목은 목과 스텁, 두가지 유형 테스트 대역을 생성할 수 있기 때문에 도구로서의 목과 테스트 대역으로서 목을 혼동하지말자.
- 목 (+ 스파이)
- 결과적으로 외부로 나가는 행위는 목을 사용할 수 밖에 없는 것으로 보임.
- Why ? 외부(서비스) 는 우리의 의존성이 아니니 확인이 불가능 따라서, n 번 호출되었는지 호출될 때, 원하는 값이 들어 있는지 주로 체크하게 됨
- 스텁은 상호 작용 검증에 어울리지 않음
- 내부로 들어오는 행위를 위해서 사용하기 때문에 최종 결과가 아닐 가능성이 높음 / 스텁의 상호 작용을 검증하는 것은 안티 패턴임
- 상호 작용을 검증하게 되면 결과는 변경되지 않아도 내부로 들어오는 행위가 변경되면 테스트가 깨지게 됨 이는 곧 4장에서 이야기한 리팩토링 내성이 약해지는 것을 의미 / 불필요한 테스트 실패가 늘어나게 됨.
- 내부로 들어오는 행위를 위해서 사용하기 때문에 최종 결과가 아닐 가능성이 높음 / 스텁의 상호 작용을 검증하는 것은 안티 패턴임
- 세부 구현체를 숨기는 잘 작성된 API 는 테스트를 작성하는데 도움을 주고, 잘 작성된 테스트는 API 가 잘 설계 될 수 있도록 해줌 둘은 긍정의 상관관계가 있음
- 목은 어플리케이션 경계를 넘나드는 상호 작용을 검증할 때, 의미 있음. 그외에는 세부 내용에 대한 검증이라 결과는 변경되지 않아도 테스트는 실패할 수 있음(잦은 테스트 실패로 신뢰성 저하)
6장
- 단위 테스트를 작성하는 3가지 스타일
- 하나의 테스트에서는 아래 3가지 스타일 모두를 함께 사용할 수 있음.
- 출력 기반 테스트
- 테스트 대상 시스템(SUT)에 입력을 넣고 생성되는 출력을 점검하는 방식(사이드 이펙트가 없음, 따라서 함수형 프로그래밍과 관련이 깊음)
- 상태 기반 테스트
- 작업 완료 후 시스템의 상태를 확인해서 검증하는 방식
- SUT 등 외부 상태를 직접 조회해서 확인하는 방식
- 상태라는 용어는 SUT 또는 데이터 베이스, 파일 시스템과 같은 외부 의존성 상태를 의미할 수 있음
- 작업 완료 후 시스템의 상태를 확인해서 검증하는 방식
- 통신 기반 테스트
- 목을 이용해 SUT 와 협력자 간의 통신을 검증하는 방식
- 결과가 외부 시스템에 있고 외부 시스템에 접근하기 어려운 경우 사용하는 방식
- 안드로이드에서 로그인 하는 경우 로그인 서버는 외부 시스템에 존재하기 때문에, login 메서드가 정상적으로 호출 되었으면 성공했을 것이라는 가정
- 출력 기반 테스트
- 역시나(?) 고전파는 상태 기반 스타일을 선호/런던파는 통신 기반 스타일 선호
- But 둘다 출력 기반 테스트 사용
- 좋은 단위 테스트 4대 요소에서 바라본다면 ?
- 회귀 방지, 빠른 피드백 : 어떤 스타일이든 비슷, 목이 수만개이상 된다면 약간의 지연이 발생할 수 있음
- 리팩터링 내성 : 코드 세부 사항과 결합이 적은 출력 기반이 유리 / 상태 기반 테스트는 외부 시스템 등의 상태, 값을 검증해야하는 경우가 생기기 때문에 상대적으로 떨어질 수 있음 / 통신 기반의 경우 세부 구현이 호출되었는지 검증하기 때문에 거짓 양성이 발생하기 쉬움 → 캡슐화를 잘 지키고, 식별가능한 동작에만 결합해서 최소한으로 관리하자
- 유지보수성 : 출력 기반 테스트가 유리 (코드 자체가 간결하고, 입력과 검증 로직만 확인하면 됨) / 상태 기반의 경우 여러 상태를 검증하면 코드량이 늘어나고, 헬퍼 등 유틸 테스트 함수(클래스 속성 값 비교)로 관리할 수 있지만, 유지보수 해야할 코드가 늘어나기 때문에 불리한 부분이 존재 / 통신 기반은 목을 작성해야하니 기본적으로 코드량이 많은편이고, 목을 위해 목을 사용하는 목 체인이 있는 경우 좀더 분리할 수 있음
'한달에 교양 책 한권' 카테고리의 다른 글
일 잘하는 팀장 (1) | 2024.10.26 |
---|---|
머니북 (0) | 2024.07.06 |
켄트벡의 Tidy First? (0) | 2024.04.27 |
읽기 좋은 코드가 좋은 코드다 (0) | 2024.04.20 |
육각형 개발자 (0) | 2024.02.18 |