Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chap10,11 학습 정리 #25

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions Chapter10/뽀로로.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# 10.단위 테스트의 원칙

---

## 10.1 단위 테스트 기초
- 테스트 중인 코드: `실제 코드`
- 테스트 코드: 단위 테스트를 구성하는 코드
- 테스트 케이스
- 준비 - (given)
- 실행 - (when)
- 단언 - (then)
- 테스트 러너

## 10.2 좋은 단위 테스트는 어떻게 작성할 수 있는가?
- 훼손의 정확한 감지: 코드가 훼손되면 테스트가 실패한다. 그리고 테스트는 코드가 실제로 훼손된 경우에만 실패해야 한다.
- 세부 구현 사항에 독립적: 세부 구현 사항을 변경하더라도 테스트 코드는 변경하지 않는 것이 이상적이다.
- 잘 설명되는 실패: 코드가 잘못되면 테스트는 실패의 원인과 문제점을 명확하게 설명해야 한다.
- 이해할 수 있는 테스트 코드: 다른 개발자들이 테스트 코드가 정확히 무엇을 테스트하기 위한 것이고 테스트가 어떻게 수행되는지 이해할 수 있어야 한다.
- 쉽고 빠르게 실행: 개발자는 일상 작업 중에 단위 테스트를 자주 실행한다. 단위 테스트가 느리거나 실행이 어려우면 개발 시간이 낭비된다.

### 10.2.1 훼손의 정확한 감지
단위 테스트의 가장 명확하고 주된 목표는 코드가 훼손되지 않았는지 확인하는 것이다.
테스트 중인 코드가 훼손되면 테스트가 실패해야 한다. 이것은 두 가지 역할을 수행한다.
- 코드에 대한 초기 신뢰를 준다.
- 미래의 훼손을 막아준다.

### 10.2.2 세부 구현 사항에 독립적
일반적으로 개발자가 코드베이스에 가할 수 있는 변경은 두 가지 종류가 있다.
- 기능적 변화: 코드가 외부로 보이는 동작을 수정. 예) 새로운 기능 추가, 버그 수정, 에러 처리
- 리팩터링

> 기능 변경과 리팩터링을 같이 하지 말라

: 코드 베이스를 변경할 때 일반적으로 기능만 변경하거나 리팩터링만 해야지 두 가지 작업을 동시에 수행하는 것은 좋지 않다.
리팩터링은 어떠한 동작도 변경하지 않지만, 기능 변경은 동작을 변경한다. 기능적 변화와 리팩터링을 동시에 하면
기능적 변화로 예상되는 동작의 변화와 리팩터링의 실수로 발생하는 동작의 변화를 구분하기 어려울 수 있다.
보통 리팩터링을 한 다음 기능 변경을 따로 하는 것이 좋다. 이렇게 하면 잠재적인 문제의 원인을 분리하기가 훨씬 더 쉬워진다.

### 10.2.3 잘 설명되는 실패
테스트 실패가 잘 설명되도록 하는 좋은 방법 중 하나는 하나의 테스트 케이스는 한 가지 사항만 검사.
각 테스트 케이스에 대해 서술적인 이름을 사용.

-> 이렇게 하면 한 번에 모든 것을 테스트하려고 하는 하나의 큰 테스트 케이스보다 각각의 특정 동작을 확인하기 위한 작은 테스트 케이스가 많이 만들어진다.

### 10.2.4 이해 가능한 테스트 코드

### 10.2.5 쉽고 빠른 실행
테스트를 빠르고 쉽게 유지해야 하는 이유는
- 느린 단위 테스트는 개발자의 작업 속도를 느리게 만든다.
- 개발자가 실제로 테스트를 할 수 있는 기회를 극대화하기 위함이다.

## 10.3 퍼블릭 API에 집중하되 중요한 동작은 무시하지 말라
가능하면 퍼블릭 API를 사용하여 코드의 동작을 테스트해야 한다.
이는 순전히 퍼블릭 함수의 매개변수, 반환값, 오류 전달을 통해 발생하는 동작만 테스트해야 한다는 의미

그러나 코드의 퍼블릭 API를 어떻게 정의하느냐에 따라 퍼블릭 API만으로는 모든 동작을 테스트할 수 없는 경우가 있다.
- 서버와 상호작용하는 코드
- 데이터베이스에 값을 저장하거나 읽는 코드

## 10.4 테스트 더블
테스트 더블은 의존성을 시뮬레이션하는 객체지만 테스트에 더 적합하게 사용할 수 있도록 만들어진다.
- 목
- 스텁
- 페이크

### 10.4.1 테스트 더블을 사용하는 이유
- 테스트 단순화
- 테스트로부터 외부 세계 보호
- 외부로부터 테스트 보호

### 10.4.2 목
클래스나 인터페이스를 시뮬레이션하는 데 멤버 함수에 대한 호출을 기록하는 것 외에는 어떠한 일도 수행하지 않는다.
함수가 호출될 때 인수에 제공되는 값을 기록한다.

`예) BankAccount mockAccount = createMock(BankAccount);`
다음과 같이 목 객체를 생성하여 사용

단점으로는 테스트가 비현실적이고 중요한 버그를 잡지 못할 위험이 있다.

### 10.4.3 스텁
함수가 호출되면 미리 정해 놓은 값을 반환함으로써 함수를 시뮬레이션

### 10.4.4 목과 스텁은 문제가 될 수 있다
주요 단점
- 목이나 스텁이 실제 의존성과 다른 방식으로 동작하도록 설정되면 테스트는 실제적이지 않다.
- 구현 세부 사항과 테스트가 밀접하게 결합하여 리팩터링이 어려워질 수 있다.

### 10.4.5 페이크
페이크는 클래스의 대체 구현체로 테스트에서 안전하게 사용할 수 있다.
외부 시스템과 통신하는 대신 페이크 내의 멤버 변수에 상태를 저장한다.

페이크를 사용하면 목과 스텁에 대한 문제점을 피할 수 있다.
- 페이크로 인해 보다 실질적인 테스트가 이루어질 수 있다.
- 페이크를 사용하면 구현 세부 정보로부터 테스트를 분리할 수 있다.

## 요약
- 코드베이스에 제출된 거의 모든 '실제 코드'는 그에 해당하는 단위 테스트가 동반되어야 한다.
- '실제 코드'가 보여주는 모든 동작에 대해 이를 실행해보고 결과를 확인하는 테스트 케이스가 작성되어야 한다. 아주 간단한 테스트 케이스가 아니라면 각 테스트 케이스 코드는 준비, 실행 및 단언의 세 가지 부분으로 나누는 것이 일반적이다.
- 바람직한 단위 테스트의 주요 특징은 다음과 같다.
- 문제가 생긴 코드의 정확한 탐지
- 구현 세부 정보에 구애받지 않음
- 실패가 잘 설명됨
- 이해하기 쉬운 테스트 코드
- 쉽고 빠르게 실행
- 테스트 더블은 의존성을 실제로 사용하는 것이 불가능하거나 현실적으로 어려울 때 단위 테스트에 사용할 수 있다.
- 목
- 스텁
- 페이크
- 목 및 스텁을 사용한 테스트 코드는 비현실적이고 구현 세부 정보에 밀접하게 연결될 수 있다.
- 목과 스텁의 사용에 대한 여러 의견이 있다. 필자의 의견은 가능한 한 실제 의존성이 테스트에 사용되어야 한다는 것이다. 이렇게 할 수 없다면, 페이크가 차선책이고, 목과 스텁은 최후의 수단으로만 사용되어야 한다.
112 changes: 112 additions & 0 deletions Chapter11/뽀로로.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# 11. 단위 테스트의 실제

---

## 11.1 기능뿐만 아니라 동작을 시험하라
코드가 보이는 중요한 행동을 모두 테스트해야 한다.

함수별로 테스트 케이스를 하나만 작성하면 중요한 동작을 놓칠 수 있다.
단순히 눈에 보이는 대로 함수 이름을 테스트 목록에 넣기보다는 함수가 수행하는 모든 동작으로 목록을 채우는 것이 좋다.

### 11.1.1 함수당 하나의 테스트 케이스만 있으면 적절하지 않을 때가 많다
다른 많은 사항을 테스트하지 않은 채로 남겨두면 충분한 테스트가 아니다.

해결책: 각 동작을 테스트하는 데 집중하라

코드에 대한 신뢰도를 높이기 위해 여러 가지 값과 경계 조건을 테스트하는 것이 타당하다.

테스트를 올바르게 확인하기 위해 다음과 같이 생각해야 된다.
- 모든 동작이 테스트되었는지 거듭 확인하라
- 오류 시나리오를 잊지 말라

## 11.2 테스트만을 위해 퍼블릭으로 만들지 말라
프라이빗 함수는 구현 세부 사항이면 클래스 외부의 코드가 인지하거나 직접 사용하는 것이 아니다.
때로는 이러한 프라이빗 함수 중 일부를 테스트 코드에서도 접근할 수 있도록 만들어 직접 테스트하고자 할 수 있다.
그러나 이는 **좋은 생각이 아닐 때**가 많다.
구현 세부 사항과 밀접하게 연관된 테스트가 될 수 있고 궁극적으로 우리가 신경 써야 하는 코드의 동작을 테스트하지 않을 수 있기 때문이다.

### 11.2.1 프라이빗 함수를 테스트하는 것은 바람직하지 않을 때가 많다.
프라이빗 함수를 퍼블릭으로 만든 후에 테스트할 때의 문제는 다음과 같다.
- 실제로 우리가 신경 쓰는 행동을 테스트하는 것이 아니다
- 구현 세부 사항에 독립적이지 못하게 된다.
- 퍼블릭 API를 변경한 효과를 갖는다.

좋은 단위 테스트는 궁극적으로 중요한 행동을 테스트해야 한다.
이렇게 하면 테스트는 코드의 문제점을 정확하게 감지할 가능성을 극대화하며 구현 세부 사항에 독립적으로 된다.

### 11.2.2 해결책: 퍼블릭 API를 통해 테스트하라 && 11.2.3 해결책: 코드를 더 작은 단위로 분할하라
비교적 간단한 클래스의 경우 퍼블릭 API만을 사용하여 모든 동작을 테스트하기가 매우 쉽다.
그러나 클래스가 더 복잡하거나 많은 논리를 포함하면 퍼블릭 API를 통해 모든 동작을 테스트하는 것이 까다로울 수 있다.
이 경우는 코드의 추상화 계층이 너무 크다는 것을 의미하기 때문에 코드를 더 작은 단위로 분할하는 것이 유익하다.

## 11.3 한 번에 하나의 동작만 테스트하라
여러 동작을 한꺼번에 테스트하면 테스트가 제대로 안 될 수 있다.

모든 것을 한꺼번에 테스트하는 테스트 케이스는 정확히 무엇이 변경됐는지 알려주는 대신, 무언가 변경됐다는 것만 알려준다.
따라서 코드를 의도적으로 변경할 때 그 변경으로 인해 어떤 동작이 영향을 받았고 어떤 동작이 영향을 받지 않았는지 정확히 알기 어렵다.

>해결책: 각 동작은 자체 테스트 케이스에서 테스트하라

각 동작을 개별적으로 테스트하고 각 테스트 케이스에 적절한 이름을 사용하면 테스트가 실패할 경우 어떤 동작이 실패했는지 잘 알 수 있다.

각 동작을 하나의 테스트 케이스로 테스트하면 장점이 있지만, 코드 중복이 많아지는 단점도 있다.
이런 코드 중복을 줄이는 한 가지 방법은 매개변수화된 테스트를 사용하는 것이다.

## 11.4 공유 설정을 적절하게 사용하라
테스트 케이스는 의존성을 설정하거나 테스트 데이터 저장소에 값을 채우거나 다른 종류의 상태를 초기화하는 등 어느 정도의 설정이 필요할 때가 있다.

테스트 프레임워크에서 테스트 케이스 간에 이 설정을 쉽게 공유할 수 있는 기능을 제공한다.
- BeforeAll: 테스트 케이스가 실행되기 전에 단 한 번 실행된다. 'OneTimeSetUp'
- BeforeEach: 각 테스트 케이스가 실행되기 전에 매번 실행한다. 'SetUp'

공유 설정을 해체하는 방법도 제공한다.
- AfterAll: 모든 테스트 케이스가 실행된 후 코드가 한 번 실행된다. 'OneTimeTearDown'
- AfterEach: 각 테스트 케이스가 실행된 후에 매번 실행된다. 'TearDown'

이와 같은 설정 코드 블록을 사용하면 설정을 서로 다른 테스트 케이스 간에 공유할 수 있다.
이것은 두 가지 중요한 방식으로 일어날 수 있다.
- 상태 공유
- 설정 공유

### 11.4.1 상태 공유는 문제가 될 수 있다.
일반적으로 테스트 케이스는 서로 격리되어야 하므로 한 테스트 케이스가 수행하는 모든 조치는 다른 테스트 케이스의 결과에 영향을 미치지 않아야 한다.
테스트 케이스 간에 상태를 공유하고 이 상태가 가변적이면 이 규칙을 실수로 위반하기 쉽다.

해결책: 상태를 공유하지 않거나 초기화하라

### 11.4.3 설정 공유는 문제가 될 수 있다.
설정을 공유하면 어떤 테스트 케이스가 어떤 특정 항목에 의존하는지 정확하게 추적하는 것은 매우 어려우며,
향후 변경 사항이 발생하면 테스트 케이스가 원래 목적했던 동작을 더 이상 테스트하지 않게 될 수 있다.

해결책: 중요한 설정은 테스트 케이스 내에서 정의하라

## 11.5 적절한 어서션 확인자를 사용하라
부적합한 확인자는 테스트 실패를 잘 설명하지 못할 수 있다.

테스트는 코드에 정말로 문제가 있을 때에만 테스트가 실패하고 `실패의 이유가 잘 설명`돼야 한다.
이 목표를 달성하기 위해 적절한 어서션 확인자를 선택할 필요가 있다.

## 11.6 테스트 용이성을 위해 의존성 주입을 사용하라
하드 코딩된 의존성은 테스트를 불가능하게 할 수 있다.

>해결책: 의존성 주입을 사용하라

의존성 주입은 코드를 좀더 모듈화하기 위한 효과적인 기술이며, 따라서 코드의 테스트 용이성을 높이기 위한 효과적인 기술이다.

## 11.7 테스트에 대한 몇 가지 결론
개발자로서 접하고 사용할 가능성이 큰 다른 두 가지 테스트 수준은 다음과 같다.
- 통합 테스트(integration test): 한 시스템은 일바적으로 여러 구성 요소, 모듈, 하위 시스템으로 구성된다. 이러한 구성 요소와 하위 시스템을 서로 연결하는 프로세스를 통합이라고 한다. 통합 테스트는 이러한 통합이 제대로 작동하는지 확인하기 위한 테스트다.
- 종단 간 테스트(end-to-end test): 처음부터 끝까지 전체 소프트웨어 시스템을 통과하는 여정을 테스트한다.

다양한 유형의 테스트
- 회귀 테스트(regression test): 소프트웨어의 동작이나 기능이 바람직하지 않은 방식으로 변경되지 않았는지 확인하기 위해 정기적으로 수행하는 테스트이다.
- 골든 테스트(golden test): 특성화 테스트라고도 하며, 일반적으로 주어진 입력 집합에 대해 코드가 생성한 출력을 스냅샷으로 저장한 것을 기반으로 한다. 테스트 수행 후 코드가 생성한 출력이 다르면 테스트는 실패한다.
- 퍼즈 테스트(fuzz test): 많은 무작위 값이나 흥미로운 값으로 코드를 호출하고 그들 중 어느 것도 코드의 동작을 멈추지 않는지 점검한다.

## 요약
- 각 함수를 테스트하는 것에 집중하다 보면 테스트가 충분히 되지 못하기 쉽다. 보통은 모든 중요한 행동을 파악하고 각각의 테스트 케이스를 작성하는 것이 더 효과적이다.
- 결과적으로 중요한 동작을 테스트해야 한다. 프라이빗 함수를 테스트하는 것은 거의 대부분 결과적으로 중요한 사항을 테스트하는 것이 아니다.
- 한 번에 한 가지씩만 테스트하면 테스트 실패의 이유를 더 잘 알 수 있고 테스트 코드를 이해하기가 더 쉽다.
- 테스트 설정 공유는 양날의 검이 될 수 있다. 코드 반복이나 비용이 큰 설정을 피할 수 있지만 부적절하게 사용할 경우 효과적이지 못하거나 신뢰할 수 없는 결과를 초래할 수 있다.
- 의존성 주입을 사용하면 코드의 테스트 용이성이 상당히 향상될 수 있다.
- 단위 테스트는 개발자들이 가장 자주 다루는 테스트 수준이지만 이것만이 유일한 테스트는 아니다. 높은 품질의 소프트웨어를 작성하고 유지하려면 여러 가지 테스트 기술을 함께 사용해야 할 때가 많다.