--- 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. 결론 테스트는 커버리지가 넓을수록 시너지가 좋다. 그러려면 팀 전체가 함께 할수록 좋은데 바쁜와중에 그러기가 쉽지 않다. 강제 해 봤자 커버리지만 넓고 정작 중요한 테스트는 놓치는 경우도 있고 읽기도 힘든 테스트가 만들어 진다. 읽기 힘든 테스트 스윗을 쭉 보고 있노라면 지워버리고 싶은 심정이다. 장기적으로 서로의 테스트를 리뷰 해주고 갈고닦는 과정이 필요하다.