Skip to content
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
521 changes: 521 additions & 0 deletions parkjyun/docs/WEEK1.md

Large diffs are not rendered by default.

325 changes: 325 additions & 0 deletions parkjyun/docs/박재연2주차.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
# /

# 테스트 코드 작성 순서

## 초반에 복잡한 테스트부터 시작하면 안 되는 이유

testcode 2차

1. 모든규칙을충족하는암호강도는'강함'
2. 길이만8글자미만이고나머지규칙은충족하는암호의강도는'보통' 3. 숫자를포함하지않고나머지규칙은충족하는암호의강도는'보통' 4. 값이없는암호의강도는'유효하지않음'
3. 대문자를포함하지않고나머지규칙은충족하는경우
4. 길이가8글자이상인규칙만충족하는경우
5. 숫자 포함 규칙만 충족하는 경우
6. 대문자포함규칙만충족하는경우
7. 아무규칙도충족하지않는경우

위의 순서로 암호 강도 측정 기능을 구현했다.

다음과 같은 규칙으로 위의 순서를 정한 것임

- 쉬운 경우에서 어려운 경우로 진행

쉬운 경우란 구현이 쉬운 것을 의미함 → 3가지 조건 만족할 때 강함 → 그냥 Strong 리턴하면 되니 구현 쉬움

반대로 하나의 규칙만 만족하는 경우는 → 3가지 규칙을 모두 구현한 후 하나의 규칙만 만족하는지 검사해야함

- 예외적인 경우에서 정상적인 경우로 진행

만약 초반에 다양한 조합을 검사하는 복잡한 상황을 테스트로 추가하면 해당 테스트를 통과하기 위해 구현해야 할 코드가 많아진다.

예를 들어

- 대문자 포함 규칙만 충족하는 경유
- 모든 규칙을 충족하는 경우
- 숫자를 포함하지 않고 나머지 규칙은 충족하는 경우

다음과 같은 순서로 테스트 코드를 작성한다면

첫번째 경우는 그냥 Weak return

두번째 경우는 ..? 아마 모든 규칙을 확인하는 코드를 전부 작성해야함

→한번에 많은 코드를 작성하다 보면 버그가 생길 수 있다.

## 구현하기 쉬운 테스트부터 시작하기

그래서 초반에 구현이 복잡한 테스트코드부터 작성하는 것이 아니라

구현하기 쉬운 걍우부터 시작해야한다.

암호 강도 측정기에서는 다음 경우가 쉬울 것이다.

- 모든 조건을 충족하는 경우
- 모든 조건을 충족하지 못하는 경우

두 가지 모두 그냥 해당 값을 리턴하면 된다.

우리는 **모든 조건을 충족하는 경우를** 먼저 구현했다.

그 다음은 무엇을 골라야 할까?

- 모든 규칙을 충족하지 않는 경우 → 모든 규칙을 검사하는 코드를 구현해야함 → 구현 넘 많아
- 한 규칙만 충족하는 경우 → 규칙 하나만 구현 후 충족하는지 검사
- 두 규칙을 충족하는 경우. → 한 규칙만 구현후 해당 규칙 충족하지 않으면 normal 리턴

첫번째 상황보다는 두번째, 세번째 상황이 구현해야 할 코드가 적다.

이때 3가지 규칙 중 어떤 규칙을 검사하는 것이 쉬울까?

- 대문자 포함 여부
- 숫자 포함 여부
- 8글자 이상인지

세번째(8글자 이상인지)가 구현이 가장 쉬울 것이다.

그래서 해당 규칙을 체크하는 **8글자 미만, 나머지 규칙을 충족하는 암호는 보통**의 상황을 두번째 경우로 체크했다.

## 예외 상황을 먼저 테스트애햐 하는 이유

예외 사황은 복잡한 if-else블록을 동반할 때가 많다.

예외 상황을 전혀 고려하지 않은 코드에 예외 상황을 반영하려면 코드의 구조를 뒤집거나 코드 중간에 예외 상황을 처리하기 위한 조건문을 중복해서 추가하는 일이 벌어진다.

이는 코드를 복잡하게 만들어 버그 발생 가능성을 높인다.

초반에 예외 상황을 테스트하면 이런 가능성이 줄어든다.

예외 상황에 따른 If-else구조가 미리 만들어지기 때문에 많은 코드를 완성한 뒤에 예외 상황을 반영할 때보다 코드 구조가 덜 바뀐다.

## 완급조절

- 정해진 값을 리턴
- 값 비교를 이용해서 정해진 값을 리턴
- 다양한 테스트를 추가하면서 구현을 일반화

다음 단계에 따라 tdd를 하면 구현이 복잡한 코드를 조금씩 단계를 나누어 구현해 나갈 수 있다.

위의 단계가 불필요하다고 느껴질 수 있지만 위의 단계로 나누어 구현하는 것에 익숙해져야 나중에 복잡한 구현까지 쪼개어 할 수 있게 된다.

## 지속적인 리팩토링

**테스트 코드와 구현 코드 모두** 테스트를 통과한 후에 지속적인 리팩토링을 진행해야한다.

테스트 대상 코드의 리팩토링 시점 :

- 상수를 변수로 바꾸거나 변수 이름을 변경하는 것과 같은 작은 ㄹ팩토링은 발견하면 바로 실행한다.
- 반면에 메서드 추출과 같이 메서드의 구조에 영향을 주는 리팩토링은 큰 틀에서 구현 흐름이 눈에 들어오기 시작한 뒤에 진행한다.(구현 초기에는 아직 구현의 전반적인 흐름을 모르기 때문에 메서드 추출과 같은 리팩토링을 진행하면 코드 구조를 잘못 잡게 될 가능성이 있다.)

# 테스트 작성 순서 연습

매달 비용을 지불해야 사용할 수 있는 유료 서비스가 있다고 하자. 다음과 같은 규칙을 가진다.

- 서비스를 사용하려면 매달 1만원을 선불로 납부한다. 납부일 기준으로 한 달뒤가 서비스 만료일이 된다.
- 2개월 이상 요금을 납부할 수 있다.
- 10만원을 납부하면 서비스를 1년 제공한다.

## 쉬운것부터 테스트

다음과 같은 규칙을 기준으로 테스트 코드를 작성해나가자 했다.

- 구현하기 쉬운 것부터 먼저 테스트
- 예외 상황을 먼저 테스트

1번 규칙(만원 내면 서비스 만료일은 한달 뒤가 된다)이 가장 구현하기 쉬울 것 같다.

```jsx
public class ExpiryDateCalculator {
public LocalDate calculateExpiryDate(LocalDate billingDate, int payAmount) {
return LocalDate.of(2019,4,1);
}
}
```

다음과 같이 상수를 return 하게 하고

```jsx
@Test
void 만원_납부하면_한달_뒤가_만료일이_됨() {
LocalDate billingDate = LocalDate.of(2019, 3, 1);
int payAmount = 10_000;

ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate expiryDate = cal.calculateExpiryDate(billingDate, payAmount);

assertEquals(LocalDate.of(2019, 4, 1), expiryDate);
}
```

다음과 같은 테스트 코드를 만들어서 일단 통과 시킴.

## 예를 추가하면서 구현을 일반화

```jsx
@Test
void 만원_납부하면_한달_뒤가_만료일이_됨() {
LocalDate billingDate = LocalDate.of(2019, 3, 1);
int payAmount = 10_000;

ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate expiryDate = cal.calculateExpiryDate(billingDate, payAmount);

assertEquals(LocalDate.of(2019, 4, 1), expiryDate);

LocalDate billingDate2 = LocalDate.of(2019, 5, 5);
int payAmount2 = 10_000;

ExpiryDateCalculator cal2 = new ExpiryDateCalculator();
LocalDate expiryDate2 = cal2.calculateExpiryDate(billingDate2, payAmount2);

assertEquals(LocalDate.of(2019, 6, 5), expiryDate2);

}
```

다음과 같은 테스트 코드를 추가하고

ExpiryDateCalculator에서 상수를 return 하는 대신에 구현을 일반화한다.

```jsx
public class ExpiryDateCalculator {
public LocalDate calculateExpiryDate(LocalDate billingDate, int payAmount) {
return billingDate.plusMonths(1);
}
}
```

## 리팩토링

테스트를 통과했으니 리팩토링을 진행한다.

테스트 코드와 구현코드 모두 리팩토링의 대상이다.

```jsx
public class ExpiryDateCalculatorClass {

@Test
void 만원_납부하면_한달_뒤가_만료일이_됨() {
assertExpiryDate(LocalDate.of(2019, 3, 1), 10_000, LocalDate.of(2019, 4, 1));
assertExpiryDate(LocalDate.of(2019, 5, 5), 10_000, LocalDate.of(2019, 6, 5));
}

private void assertExpiryDate(LocalDate billingDate, int payAmount, LocalDate expectedExpiryDate) {
ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate expiryDate = cal.calculateExpiryDate(billingDate, payAmount);
assertEquals(expectedExpiryDate, expiryDate);
}
}
```

테스트 코드를 다음과 같이 리팩토링한다.

/

## 예외상황처리

위에서 테스트 코드 작성 순서에서 구현이 쉬운 경우와 예외 상황에 대한 처리를 먼저 하기로 했었다.

이제 다음과 같은 예외 상황을 처리해보자

- 납부일이 2019-01-31이고 납부액이 만원이면 만료일은 2019-02-28이다.
- 납부일이 2019-05-31이고 납부액이 만원이면 만료일은 2019-06-30이다.
- 납부일이 2020-01-31이고 납부액이 만원이며 만료일은 2020-02-29이다.

```jsx
@Test
void 납부일과_한달_뒤_일자가_같지_않음() {
assertExpiryDate(LocalDate.of(2019, 1, 31), 10_000, LocalDate.of(2019, 2, 28));
assertExpiryDate(LocalDate.of(2019, 5, 31), 10_000, LocalDate.of(2019, 6, 30));
assertExpiryDate(LocalDate.of(2020, 1, 31), 10_000, LocalDate.of(2020, 2, 29));
}
```

예외 상황에 대해 localdate의 plus months가 알아서 처리를 해준다.

## 예외상황처리(2)

첫 납부일 일자와 만료일 일자가 같지 않을 때 만료일 계산 테스트

다음과 같은 경우가 있다

- 첫 납부일이 2019-01-31이고 만료되는 2019-02-28에 만원을 납부하면 다음 만료일은 2019-03-31이다.

## 코드 정리 : 상수를 변수로

ExpiryDateCalculator에서 1을 변수로 선언해준다.

## 다음 테스트 선택 : 쉬운 테스트

- 2만원을 지불하면 만료일이 두달뒤가 된다.
- 3만원을 지불하면 만료일이 석달뒤가 된다.

## 다음 테스트 : 10개월 요금을 납부하면 1년제공

## 테스트할 목록 정리하기

tdd시작할 때 테스트할 목록을 미리 정리하면 좋다.

예를들어 방금 구현한 만료일 계산기능에는

- 1만원 납부하면 한달 뒤가 만료일
- 달의 마지막 날에 납부하면 다음달 마지막 날이 만료일
- 2만원 납부하면 2개월 뒤가 만료일
- 3만원 납부하면 3개월 뒤가 만료일
- 10만원을 납부하면 1년 뒤가 만료일

이렇게 테스트할 목록을 정했으면 **구현이 가장 쉬운 테스크, 가장 예외적인 테스트**를 먼저 구현한다.

테스트 사례를 발견하면 목록에 추가해서 놓치지 말아야 한다.

테스트 목록에 추가했다고 한번에 많은 테스트를 다 작성하면 안 된다. → 테스트가 많으면 리팩토링에 대한 심리적 저항 → 리팩토링 못하게 됨

하나의 테스트 코드를 만들고 이를 통과시키고 리팩토링하고 다시 다음 테스트 코드를 만들고 통과시키는 과정이 좋다.

변경이 매우 큰 리팩토링은 시간이 오래 걸려 tdd의 흐름을 깨기 쉽다. 이때는 리팩토링을 진행하지 말고 테스트를 통과시키는데 집중한다.

# 시작이 안될 때는 단언부터 고민

테스트 코드를 작성하다 보면 시작이 잘 안 될 때가 있다. → 검증한느 코드부터 작성하면 도움 됨.

assertEquals(기대하는 만료일, 실제 만료일) 이런식으로 검증하는 코드부터 작성 후 나머지 테스트 코드를 작성해나가면 된다

```jsx
ExpiryDateCalculator cal = new ExpiryDateCalculator();
LocalDate realExpiryDate = cal.calculateExpiryDate(LocalDate.of(2019,7,9), 10000);
assertEquals(LocalDate.of(2019,8,9), realExpiryDate);
```

아래부터 작성해나간다 생각하면 될 듯.

# 구현이 막히면

- 구현이 쉬운 테스트, 예외적인 테스트부터 해나간다.
- 완급조절(정해진 값 리턴, 단순 비교를 통해서 정해진 값 리턴, 테스트코드를 증가시키며 일반화)하기

# TDD, 기능 명세, 설계

기능 : 입력과 결과로 나뉨. 입력은 기능을 실행하는데 필요한 값, 보통 메서드의 파라미터로

결과는 리턴값, 예외, 변경(ex.디비의 변경)이 모두 포함된다.

##

# 설계 과정을 지원하는 TDD

tdd는 테스트를 만드는 것부터 시작.

테스트코드를 위해선 **테스트할 기능을 실행, 실행결과를 검증** 두가지가 있어야한다.

기능을 실행하기 위해서는 객체 생성을 위한 클래스가 있어야 하고 실행할 메서드도 필요. → 테스트의 대상이 되는 클래스와 메서드의 이름을 정해야 한다. 그리고 메서드의 인자의 타입과 개수도 결정해야한다.

예를들어 2장에서는 테스트 대상이 될 클래스의 이름(PasswordStrengthMeter)를 정했고 메서드의 이름(meter), 리턴 타입(PasswordStrength)등을 정했다.

즉 클래스 이름, 메서드 이름, 메서드 파라미터, 실행결과를 모두 정한 것이다.

TDD 자체가 설계는 아니지만 TDD를 하다보면 테스트 코드를 작성한느 과정에서 일부 설계를 진행하게 된다.

## 필요한 만큼 설계하기

tdd는 테스트를 통과할 만큼만 코드를 작성

미리 테스트 코드를 작성하지 않는다.

설계도 마찬가지로 필요할 것으로 예측해서 미리 설계를 유연하게 만들지 않는다.

실제 테스트 사례를 추가하고 통과시키는 과정에서 필요한 만큼 설계를 변경한다.
43 changes: 43 additions & 0 deletions parkjyun/tddstudy/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/

### IntelliJ IDEA ###
.idea
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/

### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/

### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/

### VS Code ###
.vscode/

### Mac OS ###
.DS_Store
19 changes: 19 additions & 0 deletions parkjyun/tddstudy/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
plugins {
id 'java'
}

group = 'org.example'
version = '1.0-SNAPSHOT'

repositories {
mavenCentral()
}

dependencies {
testImplementation platform('org.junit:junit-bom:5.9.1')
testImplementation 'org.junit.jupiter:junit-jupiter'
}

test {
useJUnitPlatform()
}
Binary file not shown.
6 changes: 6 additions & 0 deletions parkjyun/tddstudy/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#Wed Apr 17 02:31:33 KST 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Loading