학습 목표
JUnit 프레임워크를 좀 더 자세히 살펴보자.
"테스트 없이는 스프링도 없다."라고 할 정도로 스프링에서 테스트의 중요성은 크다고 할 수 있다.
스프링의 핵심 기능 중 하나인 스프링 테스트 모듈도 JUnit을 이용한다. 따라서 스프링의 기능을 익히기 위해서라도 JUnit는 꼭 사용할 줄 알아야 한다. JUnit는 단순하기 때문에 빠르게 작성할 수 있고 대부분 자바 IDE에서 JUnit 테스트를 손쉽게 실행할 수 있는 JUnit 테스트 지원 기능 내장하고 있어서 더욱 편하게 활용할 수 있게 해 준다.
테스트 결과의 일관성
지금까지 JUnit을 적용해서 깔끔한 테스트 코드를 만들었지만 아직은 좀 더 개선했으면 하는 아쉬운 점이 있다.
가장 불편한 점은 매번 테스트 실행 전에 DB의 user테이블 데이터를 모두 삭제해야 한다는 점이다. 가장 좋은 해결책은 addAndGet() 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서 테스트를 수행하기 이전 상태로 만들어주는 것이다.
- deleteAll()의 getCount() 추가
일관성 있는 결과를 보장하는 테스트를 만들기 위해 준비할 게 있는데, 바로 UserDao에 새로운 기능을 추가해주는 일이다.
- deleteAll
-> 첫 번째 추가할 것은 deleteAll() 메소드로, user 테이블의 모든 데이터를 삭제해주는 간단한 기능이다. 아래의 코드를 UserDao 클래스에 추가한다.
- getCount()
-> 두 번째 추가할 것은 getCount() 메소드로, user테이블의 데이터 개수를 돌려준다.
- deleteAll()과 getCount()의 테스트
기능을 추가했으니 추가한 기능에 대한 테스트도 만들어야 한다. 그런데 deleteAll()과 getCount() 메소드는 독립적으로 자동 실행되는 테스트를 만들기가 애매하다. 그래서 기존에 만든 addAndGet() 메소드를 확장하는 방법을 사용하려고 한다.
여기서 짚고 넘어가야 할 것이 있다. 먼저 deleteAll()과 getCount() 메소드를 만들긴 했지만 둘 다 검증이 안됐다. 그러면 두 메소드가 잘 작동하는지 어떻게 믿을 수 있을까? 검증이 안된 두 메소드를 넣었는데 테스트를 통과한다고 안심하면 바람직하지 못하다.
그래서 getCount() 메소드에 대한 검증 작업을 하나 더 추가한다. 두 메소드를 실행하고 나면 결과 값으로 0을 반환받을 것이다. 이때 add() 메소드로 데이터를 하나 더 넣은 뒤 getCount() 메소드를 사용해서 1이 나오면 정상적으로 작동한다고 믿어도 충분할 듯싶다.
- 동일한 결과를 보장하는 테스트
이제 테스트를 실행해보면 JUnit 뷰에 녹색 막대가 나타나면서 테스트가 성공했다고 나올 것이다. DB 테이블의 데이터를 삭제하지 않아도 기존의 데이터를 지워주도록 코드를 작성했기 때문에 같은 정보를 여러 번 넣어도 에러 없이 성공적으로 수행한다.
단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다는 점을 잊어선 안 된다. DB에 남아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하고 테스트 실행 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.
- 포괄적인 테스트
getCount() 테스트
getCount()에 대한 좀 더 꼼꼼한 테스트를 만들어보자. 이번엔 여러 개의 UserVO를 등록해가면서 getCount()의 결과를 매번 확인하겠다.
JUnit는 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다. @Test가 붙어있고 public 접근자가 있으며 리턴 값이 void형이고 파라미터가 없다는 조건을 지키기만 하면 된다.
테스트 시나리오는 이렇다.
1. user 테이블의 데이터를 지우고 getCount()로 레코드 개수가 0 임을 확인.
2. 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인.
먼저 UserVO 클래스에 한 번에 모든 정보를 넣을 수 있도록 생성자 함수를 추가해준다.
그리고 UserDaoTest에 getCount()에 대한 테스트 메소드를 아래와 같이 작성한다.
이제 테스트를 실행하면 문제없이 성공을 하면서 아래와 같이 JUnit 뷰가 나올 것이다.
주의해야 할 점은 두 개의 테스트가 어떤 순서로 실행될지는 알 수 없다는 것이다. JUnit는 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다.
addAndGet() 테스트 보완
id를 조건으로 사용자를 검색하는 기능인 get()에 대한 테스트가 부족한 느낌이 있다. 그래서 get() 메소드에 대한 테스트 기능을 좀 더 보완해보자.
addAndGet() 테스트를 위와 같이 수정하고 테스트를 실행하면 문제없이 테스트를 성공하는 것을 볼 수 있다.
이렇게 해서 get() 메소드가 기대한 대로 동작하는지를 좀 더 확신할 수 있게 됐다.
get() 예외조건에 대한 테스트
만약 get() 메소드에 전달된 id 값에 해당하는 사용자가 없다면 어떻게 될까? 그리고 이럴 땐 어떤 결과가 나오면 좋을까.
1. null과 같은 특별한 값을 리턴한다.
2. id에 해당하는 정보를 찾을 수 없다고 예외를 던진다.
두 가지 방법이 있고 각기 장단점이 있지만 여기서는 2번 방법을 써보자. 주어진 id에 해당하는 정보가 없다는 의미를 가진 예외 클래스가 하나 필요하다. 스프링이 정의한 예외인 EmptyResultDataAccessException 예외를 이용하겠다.
일반적으로 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다. 그런데 이번에는 반대로 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이고, 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다.
문제는 예외 발생 여부는 메소드를 실행해서 리턴 값을 비교하는 방법으로 확인할 수 없다는 점이다. 즉, assertThat() 메소드로는 검증이 불가능하다.
그런데 바로 이런 경우를 위해 JUnit는 예외조건 테스트를 위한 특별한 방법을 제공해준다. 일단 테스트 메소드를 하나 추가하자. 그리고 모든 데이터를 지우고 존재하지 않는 id로 get()을 호출한다. 이때 EmptyResultDataAccessException이 던져지면 성공이고 아니면 실패다.
이 테스트에서 중요한 것은 @Test 어노테이션의 expected 엘리먼트다. 여기에는 실행 중 발생하리라 기대하는 예외 클래스를 넣어주면 된다. 이 테스트는 보통의 테스트와 달리 정상적으로 테스트 메소드를 마치면 테스트가 실패하고, 지정한 예외 클래스가 던져지면 테스트에 성공한 것이다.
지금 이 테스트를 실행시키면 당연히 실패한다. get() 메소드에서 rs.next()를 실행할 때 가져올 row가 없다는 SQLException이 발생하기 때문이다. 그래서 우리는 UserDao를 수정해야 한다.
테스트를 성공시키기 위한 코드의 수정
이제 할 일은 이 테스트가 성공하도록 get() 메소드 코드를 수정하는 것이다.
테스트를 실행해보면 앞서 만든 두 개를 포함한 총 세 개의 테스트가 성공한다.
최종적으로 모든 테스트가 성공하면 새로 추가한 기능도 정상적으로 동작하고 기존의 기능에도 영향을 주지 않았다는 확신을 얻을 수 있다.
포괄적인 테스트
개발자가 테스트를 직접 만들 때 자주 하는 실수가 바로 성공하는 테스트만 골라서 만든다는 것이다. 그래서 개발자의 PC에서 테스트할 때는 정상적인 케이스만 테스트를 하기 때문에 다른 사람이 했을 때와 결과가 다를 수 있다. 그러므로 다른 사람에 의해 꼼꼼하게 준비된 시나리오에 따라 다양한 경우에 대한 전문적인 테스트가 수행될 필요가 있다.
그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. 존재하는 값이 주어졌을 때 해당 레코드를 정확히 가져오는지 테스트를 하는 것도 중요하지만, 존재하지 않는 값이 주어졌을 때 어떻게 반응할지 먼저 결정하고 이를 확인할 수 있는 테스트를 먼저 만들려고 한다면 예외적인 상황을 빠뜨리지 않고 꼼꼼한 개발이 가능하다.
- 테스트가 이끄는 개발
get() 메소드의 예외 테스트를 만드는 과정을 보면 아래의 그림처럼 첫 번째가 아닌 두 번째 과정으로 했다는 것을 알 수 있다.
UserDao 코드를 수정 -> 코드 검증을 위해 테스트를 생성 >>> X
테스트를 생성하고 실패하는 것을 확인 -> UserDao 코드를 수정 >> O
이런 순서로 개발을 진행하는 구체적인 개발 전략이 실제로 존재하고, 게다가 많은 전문적인 개발자가 이런 개발 방법을 적극적으로 사용하고 있다.
기능설계를 위한 테스트
작업을 돌이켜보면 '존재하지 않는 id로 get() 메소드를 실행하면 특정한 예외가 던져져야 한다'는 식으로 기능을 결정했다.
그러고 나서 getUserFailure() 테스트를 먼저 만들었다. 테스트할 코드도 없는데 어떻게 테스트를 만들 수 있었을까?
추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능했다.
이렇게 보면 이 테스트 코드는 잘 작성된 하나의 기능 정의서처럼 보인다. 이런 식으로 추가하고 싶은 기능을 일반 언어가 아닌 테스트 코드로 표현해서, 마치 코드로 된 설계문서처럼 만들어놓은 것이라고 생각해보자. 그러고 나서 실제 기능을 가진 애플리케이션 코드를 만들고 나면, 바로 이 테스트를 실행해서 설계한 대로 코드가 동작하는지를 빠르게 검증할 수 있다.
테스트 주도 개발
만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법이 있다. 이를 테스트 주도 개발TDD, Test Driven Development라고 한다. 또는 테스트를 코드보다 먼저 작성한다고 해서 테스트 우선 개발Test First Development라고 한다.
이는 개발자가 테스트를 만들어가면서 개발하는 방법이 주는 장점을 극대화한 방법이다.
"실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않겠다."는 것이 TDD의 기본 원칙이다.
이 원칙을 따랐다면 만들어진 모든 코드는 빠짐없이 테스트로 검증된 것이라고 볼 수 있다.
TDD의 장점 중 하나는 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다는 것이다. 테스트는 코드를 작성한 후에 가능한 한 빨리 실행할 수 있어야 한다. 그러려면 테스트 없이 한 번에 너무 많은 코드를 만드는 것은 좋지 않다. 간혹 테스트를 만들고 자주 실행하면 개발이 지연되지 않을까 염려할 수도 있다. 하지만 크게 보면 테스트 코드를 일찍 만들어서 테스트하는 편이 더 낫다는 사실을 알게 된다. 실제로 스프링이 등장하기 전에는 JavaEE 애플리케이션에 대한 테스트를 만들기 어려웠지만, 현재는 스프링이 매우 편리한 기능을 많이 제공하기 때문에 안심해도 된다.
- 테스트 코드 개선
지금부터 테스트 코드를 리팩토링 해보도록 하자. 테스트 코드를 보면 애플리케이션 컨텍스트를 만드는 부분과 컨텍스트에서 UserDao를 가져오는 부분이 반복되는 것을 볼 수 있다.
중복된 코드를 별도의 메소드로 뽑아내는 것이 가장 손쉬운 방법이지만 JUnit가 제공하는 기능을 활용해보자. 먼저 세 개의 테스트 메소드에 반복적으로 등장하는 앞의 코드를 제거하자.
@Before
중복됐던 코드를 넣을 setUp() 메소드를 만들고 테스트 메소드에서 제거한 코드를 넣어준다. 그런데 테스트 메소드에 필요한 변수 dao 변수가 setUp()메소드의 로컬 변수로 되어 있다는 점이다. 그래서 이번엔 로컬 변수인 dao를 테스트 메소드에서 접근할 수 있도록 인스턴스 변수로 변경한다.
이렇게 수정한 뒤 테스트를 실행해보면 마찬가지로 테스트가 모두 성공한다. 왜?
이를 이해하려면 JUnit 프레임 워크가 테스트 메소드를 실행하는 과정을 알아야 한다.
1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
2. 테스트 클래스의 오브젝트를 하나 만든다.
3. @Before가 붙은 메소드가 있으면 실행한다.
4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장한다.
5. @After가 붙은 메소드가 있으면 실행한다.
6. 나머지 테스트 메소드에 대해 2 ~ 5번을 반복한다.
7. 모든 테스트의 결과를 종합해서 돌려준다.
실제는 이보다 복잡한데, 간단히 7단계로 정리했다.
한 가지 기억해야 할 사항은 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다.
하나의 오브젝트만 만들어놓고 사용하는 편이 성능도 낫고 효율적인데 그렇게 하지 않는 이유는 뭘까?
JUnit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장하기 위해 매번 새로운 오브젝트를 만들게 했다. 덕분에 인스턴스 변수도 부담 없이 사용할 수 있다.
만약 테스트 메소드의 일부에서만 공통적으로 사용되는 코드가 있다면 이는 @Before를 이용하기보단 일반적인 메소드 추출 방법을 써서 메소드를 분리하고 직접 호출해 사용하도록 만드는 편이 낫다.
픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처fixture라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해두면 편리하다. UserDaoTest에서라면 dao가 대표적인 픽스처이다. 테스트 중에 add() 메소드에 전달하는 UserVO 오브젝트들도 픽스처라고 볼 수 있다. 이 부분도 중복되어 있기 때문에 @Before 메소드로 중복을 제거하자.
수정을 해도 문제없이 잘 수행되는 것을 확인할 수 있다. 다음 포스팅에서는 스프링 테스트 적용에 대해 알아보자.
'Spring > 토비의 스프링 정리' 카테고리의 다른 글
2.5 학습 테스트로 배우는 스프링 (0) | 2022.04.06 |
---|---|
2.4 스프링 테스트 적용 (0) | 2022.04.05 |
2.2 UserDaoTest 개선 (0) | 2022.04.01 |
2-1 UserDaoTest 다시보기 (0) | 2022.03.31 |
1장 오브젝트와 의존관계 (0) | 2022.03.31 |