---
# System prepended metadata

title: TDD 세미나 1부 - 테스트
tags: [test, ' tdd']

---

---
tags: test, tdd
---
TDD 세미나 1부 - 테스트
===

# 1부. 테스트
TDD를 말하기 전에 테스트 라는 단어부터 생소할 수 있다. 
TDD 가 필수라고 생각하진 않지만 테스트는 필수로 가져야할 덕목이다. 
테스트는 JUnit 에서 메소드/클래스 단위별로 잘 동작하는지 여러 input 으로 동작을 검증할수 있다.


## 1. 테스트 는 왜 해야하는가?

1. 코드 리팩토링시 사이드이펙트에 대한 걱정을 덜을수 있다. 
> 잘못된 변경은 테스트 수행시 발견된다. 이전 히스토리를 돌려내고 테스트 성공한다면 정상적으로 회귀 된다.
>> 회귀테스트라고 한다.


2. 어떠한 코드를 만들었을때 이것이 정말 의도한 대로 동작하는지는 개발자 만이 안다.  
나중에  `유지보수` 또는 `리팩토링` 을 담당해야할 사람에게는 큰 부담이다.
테스트를 통해 코드가 무얼 하는것인지 설명이 가능하며, 코드에 믿음을 줄수 있다.
> 개발 시간은 1 이지만, 읽고 수정하는 시간은 9 이다.
> 꼼꼼한 주석으로도 대체할수는 있지만, 테스트 작성보다 더 귀찮은게 주석이지 않나 싶다.


3. 테스트를 짜기 힘든 코드는 코드설계가 잘못됐을 확률이 높다.
> 대체로 잘못된 코드설계라 함은 리팩토링을 하기 힘든 코드이다.
>> 하나의 클래스가 너무많은 역할을 갖고 있는 코드
>> 제 할일을 하지 않고 다른 클래스가 책임지고 있는 코드
>> 다른 클래스에 강하게 결합 되어있어서 떼 내기가 어려운 코드


*테스트는 일종의 품질 보증서 또는 사용설명서 정도의 역할을 할수 있다.*



### 테스트는 main 메소드로 할수도 있다.
/src/test/java 에 SeminarTest 클래스를 만들고, 2개의 인자를 더해주는 sum()을 만들어 보겠다.
```java 
@Slf4j
public class SeminarTest {
    public static Integer sum(int a, int b) {
        return a + b;
    }
}
```

잘 동작하는지 다음과 같이 성공/실패를 확인할수 있다.
```java
public static void main(String[] args) {
    int result = sum(0,1);

    if (result == 1) {
        System.out.println("성공");
    } else {
        System.out.println("실패");
    }
}
```
> main 을 Run 해보면 `성공` 이라고 출력된다.


하지만 main 메소드를 이용한 테스트는 **단점** 이 있다.
1. 테스트와 결과를 읽기가 어렵다. (자세한건 아래 Junit 에서 설명)
2. main 메소드가 저러라고 있는건 아니다.
3. 빌드타임때 이런걸 매번 검증해서 결과를 리폿해주면 좋을텐데 그렇지않다.
4. Spring bean 들을 테스트할수가 없다.


## 2. 테스트 프레임워크 JUnit
JUnit 은 main 메소드를 이용한 테스트에 비해 장점이 많다.
JUnit 에서는 @Test public void 가 붙은 메소드를 Run 할수 있다.
main 메소드를 지우고 다음과 같이 @Test 메소드로 고쳐보자.

```java 
@Test
public void sum_테스트(){
    assertThat(sum(0,1)).isEqualTo(1);
}
```
> Run 초록색 막대로 성공했다고 출력된다.

참고로 assertThat 은 JUnit 기본 라이브러리 외에 다른 라이브러리들이 많다. 
가장 인기있는 assertJ 를 사용하겠다. 
pom.xml 에 아래 의존성이 있어야 사용할수 있다.

```xml
<dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.11.1</version>
      <scope>test</scope>
  </dependency>
```
maven 의 test 상태일때만 사용하는 라이브러리라서 `<scope>test</scope>` 이다.
AssertJ의 isEqualTo, contains 등을 이용할수 있게 된다.


### JUnit은 테스트와 테스트 실패 사유를 읽기 쉽게 해준다.
isEqualTo, contains 의 사용 예로 설명하겠다.

#### isEqualTo 단정문

```java 
@Test
public void sum_테스트(){
    assertThat(sum(0,1)).isEqualTo(1);
}
```
>위의 테스트를 읽어보자.  **"sum(0,1)은 1 이다."** 와 같이 쉽게 읽어진다.

잘돌아 가던 코드를 리팩토링 하다가 실수로 + 를 - 로 바꿔버렸다. 

```java 
public static Integer sum(int a, int b) {
    return a - b;
}
```
테스트를 돌려보자. 실패해서 사유를 알려준다.

```
Expected :1
Actual   :-1
```
> "너는 1을 기대 했겠지만 결과는 -1이야"

실수로 sum()을 +가 아닌 -로 구현했지만 명시적인 테스트 결과를 확인할수 있다.
만약 이런 테스트들이 모든 코드를 커버하고 있다면 리팩토링에 자신감이 생길 것이다.
+로 고치고 다시 Run 해서 성공 시키자. 
> 성공 하면 다시 정상 코드로 회귀 한 것이 된다.

이제 .isEqualTo() 외에도 다른 단정문을 알아보자.


#### contains 단정문

컬렉션을 테스트 할수 있다.

```java 
@Test
public void 배열검증_테스트(){
    List<Integer> integers = Arrays.asList(1, 2);
    assertThat(integers).contains(3);
}
```
> 위의 단정문은 **"[1,2] 에는 3이 있다."** 로 읽을수 있다.

테스트를 돌려보자.
실제로는 3이 없기 때문에 테스트가 실패하면 아래와 같이 사유를 설명해 준다.
```
Expecting:
 <[1, 2]>
to contain:
 <[3]>
but could not find:
 <[3]>
```
> 예상한다 [1,2] 에 [3]이 포함될 것을, 그러나 찾을수 없다 [3]을


isEqualTo, contains 외에도 hasSize(), startWith(), endWith() 등 여러가지 검증 Assertion들이 있다.
AssertJ 에서 제공하는 Assertion 들 인데, JUnit이 기본적으로 제공하는 Assertion 과는 모양새가 약간 다르다. (AssertJ 가 더 인기있다)
- 참고 : [AssertJ](https://joel-costigliola.github.io/assertj/) 


배열검증_테스트 도 정상적으로 돌아가게 만들기 위해 코드를 수정하자. 
하는김에 다른 Assertion도 추가해 보자.
```java 

@Test
public void 배열검증_테스트(){
    List<Integer> integers = Arrays.asList(1, 2);
    assertThat(integers).contains(2);
    assertThat(integers).hasSize(2);
    assertThat(integers).startsWith(1);
    assertThat(integers).endsWith(2);
}
```
다시 테스트 돌려보면 성공.



### 테스트는 단위테스트와 통합테스트가 있다.
여태까지 알아본것처럼 메소드 하나 또는 또는 클래스 하나 가지고 여러가지 검증을 한것은 `단위테스트` 이다.

비디오샵 프로그램에서 다음과 같은 요구사항이 있다고 가정하자.
- 대여가능목록에서 비디오 1개를 대여해하면, 그 비디오는 목록에서 사라진다.


아래와 같이 대여와 목록에서 사라지는 2가지 요구를 동시에 `통합테스트` 할수 있다.



```java 
@Test
public void 빌리면_샵에서는_빌릴수있는목록이_사라진다() {

    // 원래는 대여가능목록에 있던 video1 이
    Set<Video> rentalList = shop.getRentalableList();
    assertThat(rentalList).contains(video1);

    // 빌리면
    shop.rental(video1, bonjugi, 3);

    // 목록에서 사라진다
    Set<Video> rentalListAgain = shop.getRentalableList();
    assertThat(rentalListAgain).doesNotContain((video1));
}
```
> shop.getRentalableList() 메서드와, shop.rental() 를 동시에 통합 했다.
> 하지만 기능이 복잡할수록 테스트 범위는 좁게 만드는게 좋다.



### Junit은 Spring 테스트도 가능하다.
위에까지는 POJO 만 테스트 했다. 우리는 스프링을 사용하기 때문에 ApplicationContext를 컨트롤 할 수 있어야 한다.

@Runwith 를 통해서 테스트 러너를 추가하면 된다. 
일단 테스트 러너는 테스트를 돕는 클래스 라고만 알고 넘어가자.
SpringJunit4ClassRunner 를 추가하면 ApplicationContext 의 기능을 모두 사용할수 있다. 

```java 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(RootConfig.class)
public class ActionDAOTest {
    @Autowired
    ActionDAO actionDAO; // 주입 가능
}
```
Spring 에서의 테스트는 3부에서 좀더 자세히 설명 하겠다.


### Junit은 Maven 으로 빌드시점에 테스트를 모두 돌려볼수 있다
plugin을 추가하면 Test에 src/main/test 밑의 모든 @Test 어노테이션들을 테스트하고 결과를 리포트 해준다.
> 기본적으로 Maven 패키지 컨벤션을 지키기 때문에 가능한 일
빌드시점이 아니어도 테스트파일을 여러개 잡고 Run 해도 한번에 테스트 해준다. 

테스트 케이스가 많아도 걱정이 없다.

### 결론
JUnit 과 친해지자.



## 3. 테스트는 양보단 질
양적으로 많은것보다 질적으로 좋아야 한다.
중복된 테스트 3개는 제대로된 1개보다 못하다.
단순히 분량만 늘리는게 아니라 핵심적인 부분을 커버해야 한다.
> Test suite / Test coverage 라고도 함


### 훌륭한 테스트
- 어떤 환경 에서든 깨지지 않아 함
-- 이게 생각보다 어렵다. 테스트하기 위한 값,환경 등 모든것을 FIXTURE 라고도 하는데 나중에 다시.
- 읽기 쉬워야 함
-- 테스트 메소드는 한글로 써도 된다.
-- 적절한 Assertion 을 이용해서 리폿결과를 읽기 쉽게 하자.
- 여러가지 관점으로 테스트를 수행해야 한다.
-- 사람이 하기 힘든 100개,1000개 이상의 input 을 시도 해볼수 있다.
-- sum() 에 이상한값을 넣어본다 (음수라던가 소수라던가)


#### 어떤 환경에서든 깨지지 않아야 함
어떨때 깨질까?
```java 
List<Some> somes = someDao.getSomes();
assertThat(somes).hasSize(3);
```
> somes 의 사이즈가 3이다.

이런 테스트는 만들땐 통과하더라도 언젠가 somes에 또다른 some 이 추가되면 깨지게 된다.
직접 테스트 대상 3개를 @Before 시점에 insert 한 후 Rollback 하는것이 좋다.

#### 읽기 쉬워야 함
아까의 배열검증_테스트를 읽기 어렵게 바꿔봤다.
```java 
@Test
public void 배열검증_테스트(){
    List<Integer> integers = Arrays.asList(1, 2);
    // assertThat(integers).contains(3);
    
    boolean isContain = false;
    for (Integer integer : integers) {
        if(integer.equals(3)){
            isContain = true;
            break;
        }
    }
    
    assertThat(isContain).isTrue();
}
```
> 테스트의 길이가 길기도 하고, 분석해야 한다. 
> 만약에 테스트가 맘에 들게 짜지지 않는다면 코드의 설계가 잘못되진 않았는지 의심 해 볼수 있다.
 
또한 실패사유도 불친절 해진다.

```
Expected :true
Actual   :false
```

적절한 단정문을 통해 테스트와 실패결과를 읽기 쉽게 만들자.


## 4. DevOps 에서의 테스트
미리 말하자면 먼 미래에 테스트 자동화가 되었을때의 얘기 이다.

- git push 시점에 변경된 소스들로 테스트들을 모두 돌려보고 리포팅 해준다.
- pull reqeust 시 커미터 또는 리뷰어는 테스트로 production 코드가 뭘 원하는지 알수 있다. 
- Test suite가 빈약하면 반려할수도 있다.
- merge 가 된 후에도 테스트가 자동으로 돌아가서 리포팅 해준다.






## 5. 결론
테스트는 커버리지가 넓을수록 시너지가 좋다.
그러려면 팀 전체가 함께 할수록 좋은데 바쁜와중에 그러기가 쉽지 않다.
강제 해 봤자 커버리지만 넓고 정작 중요한 테스트는 놓치는 경우도 있고 읽기도 힘든 테스트가 만들어 진다.
읽기 힘든 테스트 스윗을 쭉 보고 있노라면 지워버리고 싶은 심정이다.

장기적으로 서로의 테스트를 리뷰 해주고 갈고닦는 과정이 필요하다.

