토비의 스프링 Vol.1 - 2장 테스트
테스트
테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서, 만든 코드를 확신할 수 있게 해주는 작업이다. 또한 테스트의 결과가 원하는 대로 나오지 않는 경우에는 코드나 설계에 결함이 있음을 알 수 있다. 이를 통해 코드의 결함을 제거해가는 작업, 일명 디버깅을 거치게 되고, 결국 최종적으로 테스트가 성공하면 모든 결함이 제거됐다는 확신을 얻을 수 있다.
UserDaoTest 다시 보기
- 자바에서 가장 손쉽게 실행 가능한 main() 메소드를 이용한다.
- 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출한다.
- 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어준다.
- 테스트의 결과를 콘솔에 출력해준다.
- 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공 메시지로 출력해준다.
main() 메소드를 이용해 쉽게 테스트 수행을 가능하게 했다.
웹을 통한 DAO 테스트 방법의 문제점
웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다. DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는 점이 가장 큰 문제다.
작은 단위의 테스트
테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다. 관심사의 분리라는 원리가 여기에도 적용된다.
작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트라고 한다.
일반적으로 단위는 작을수록 좋다.자동수행 테스트 코드
UserDaoTest의 한 가지 특징은 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업 역시 코드를 통해 자동으로 실행된다는 점이다. 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
자동으로 수행되는 테스트의 장점은 자주 반복할 수 있다는 것이다. 번거로운 작업이 없고 테스트를 빠르게 실행할 수 있기 때문에 언제든 코드를 수정하고 나서 테스트를 해 볼 수 있다. 수정 때문에 다른 기능에 문제가 발생하는지 않는지 재빨리 확인하고, 성공한다면 마음에 확신을 얻을 수 있다.
지속적인 개선과 점진적인 개발을 위한 테스트
테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라, 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지를 확인할 수도 있다.
UserDaoTest의 문제점
- 수동 확인 작업의 번거로움
- UserDaoTest는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다. 하지만 여전히 사람의 눈으로 확인하는 과정이 필요하다.
add()에서 User 정보를 DB에 등록하고, 이를 다시 get()을 이용해 가져왔을 때 입력한 값과 가져온 값이 일치하는지를 테스트 코드는 확인해주지 않는다. 단지
콘솔에 값만 출력해줄 뿐이다. 결국 그 콘솔에 나온 값을 보고 등록과 조회가 성공적으로 되고 있는지를 확인하는 건 사람의 책임이다.
테스트 수행은 코드에 의해 자동으로 진행되긴 하지만 테스트의 결과를 확인하는 일은 사람의 책임이므로 완전히 자동으로 테스트되는 방법이라고 말할 수가 없다.
- UserDaoTest는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다. 하지만 여전히 사람의 눈으로 확인하는 과정이 필요하다.
add()에서 User 정보를 DB에 등록하고, 이를 다시 get()을 이용해 가져왔을 때 입력한 값과 가져온 값이 일치하는지를 테스트 코드는 확인해주지 않는다. 단지
콘솔에 값만 출력해줄 뿐이다. 결국 그 콘솔에 나온 값을 보고 등록과 조회가 성공적으로 되고 있는지를 확인하는 건 사람의 책임이다.
- 실행 작업의 번거로움
- 아무리 간단히 실행 가능한 main() 메소드라고 하더라도 매번 그것을 실행하는 것은 제법 번거롭다. 만약 DAO가 수백개가 되고 그에 대한 main() 메소드도 그만큼 만들어진다면, 전체 기능을 테스트해보기 위해 main() 메소드를 수백 번 실행하는 수고가 필요하다.
UserDaoTest 개선
모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다. 또 테스트의 실패는 테스트가 진행되는 동안에 에러가 발생해서 실패하는 경우와, 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분해볼 수 있다. 여기서 전자를 테스트 에러, 후자를 테스트 실패로 구분하자.
if(!user.getName().equals(user2.getName()) {
System.out.println("테스트 실패 (name)");
}
else if (!user.getPassword().equals(user2.getPassword())) {
System.out.println("테스트 실패 (password)");
}
else {
System.out.println("조회 테스트 성공");
}
다음과 같이 개선해서 처음 add()에 전달한 User 오브젝트와 get()을 통해 가져오는 User 오브젝트의 값을 비교해서 일치하는지 확인한다.
테스트의 효율적인 수행과 결과 관리
일정한 패턴을 가진 테스트를 만들 수 있고, 많은 테스트를 간단히 실행시킬 수 있으며, 테스트 결과를 종합해서 볼 수 있고, 테스트가 실패한 곳을 빠르게 찾을 수 있는 기능을 갖춘 테스트 지원 도구와 그에 맞는 테스트 작성 방법이 필요하다. mail() 메소드를 이용한 테스트 작성 방법만으로는 애플리케이션 규모가 커지고 테스트 개수가 많아지면 테스트를 수행하는 일이 점점 부담이 될 것이다.
JUnit 테스트로 전환
JUnit은 프레임워크다. 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아서 주도적으로 애플리케이션의 흐름을 제어한다. 개발자가 만든 클래스의 오브젝트를 생성하고 실행하는 일은 프레임워크에 의해 진행된다. 따라서 프레임 워크에서 동작하는 코드는 main() 메소드도 필요 없고 오브젝트를 만들어서 실행시키는 코드를 만들 필요도 없다.
테스트 메소드 전환
JUnit 프레임워크가 요구하는 조건 두가지
- 메소드가 public으로 선언돼야 한다.
- 메서드에 @Test라는 애노테이션을 붙여준다.
- 리턴 타입이 void로 선언돼야 한다.
- 파라미터가 없어야 한다.
public class UserDaoTest { @Test public void addAndGet() throws SQLException { ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); UserDao dao = context.getBean("userDao", UserDao.class); ... } }
검증 코드 전환
스태틱 메소드 assertThat
assertThat(user2.getName(), is(user.getName()));
assertThat() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매처(macher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어준다. is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.
JUnit은 예외가 발생하거나 assertThat()에서 실패하지 않고 테스트 메소드의 실행이 완료되면 테스트가 성공했다고 인식한다. “테스트 성공”이라는 메시지를 굳이 출력할 필요는 없다.
JUnit 테스트 실행
스프링 컨테이너와 마찬가지로 JUnit 프레임워크도 자바 코드로 만들어진 프로그램이므로 어디선가 한 번은 JUnit 프레임워크를 시작시켜 줘야 한다. 어디에든 main() 메소드를 하나 추가하고, 그 안에 JUnitCore 클래스의 main 메소드를 호출해주는 간단한 코드를 넣어주면 된다. 메소드 파라미터에는 @Test 테스트 메소드를 가진 클래스의 이름을 넣어준다.
public static void main(String[] args) { JUnitCore.main("springbook.user.dao.UserDaoTest"); }
이 클래스를 실행하면 테스트를 실행하는 데 걸린 시간과 테스트 결과, 그리고 몇 개의 테스트 메소드가 실행됐는지를 알려준다.
테스트가 실패하면 OK 대신 FAILURES!!라는 내용이 출력되고, 총 수행한 테스트 중에서 몇 개의 테스트가 실패했는지 보여준다. 출력된 메시지를 잘 살펴보면 실패한 테스트는 어떤 클래스의 어느 메소드이고, 실패의 이유는 어떠한 값을 원했는데 실행해보니 어떤 값이 나왔다는 것임을 알 수 있다.
개발자를 위한 테스팅 프레임워크 JUnit
JUnit 테스트 실행 방법
JUnitCore를 이용해 테스트를 실행하고 콘솔에 출력된 메시지를 보고 결과를 확인하는 방법은 가장 간단하긴 하지만 테스트의 수가 많아지면 관리하기가 힘들어진다는 단점이 있다. 가장 좋은 JUint 테스트 실행 방법은 자바 IDE에 내장된 JUnit 테스트 지원 도구를 사용하는 것이다.
IDE
대부분의 자바 개발자가 사용하고 있는 사실상의 표준 자바 IDE인 이클립스는 오래전부터 JUnit 테스트를 지원하는 기능을 제공하고 있다. @Test가 들어 있는 테스트 클래스를 선택한 뒤에, 이클립스 run 메뉴의 Run As 항목 중에서 JUnit Test를 선택하면 테스트가 자동으로 실행된다. JUnitCore를 이용할 때처럼 main() 메소드를 만들지 않아도 된다.
테스트가 시작되면 JUnit 테스트 정보를 표시해주는 뷰가 나타나서 테스트 진행상황을 보여준다. 이 뷰에서 테스트의 총 수행 시간, 실행한 테스트의 수, 테스트 에러의 수, 테스트 실패의 수를 확인할 수 있다. 또한 어떤 테스트 클래스를 실행했는지도 알 수 있다. 테스트 클래스 내에 있는 @Test가 붙은 테스트 메소드의 이름도 모두 보여준다. 각 테스트 메소드와 클래스의 테스트 수행에 걸린 시간도 보여준다.
테스트가 실패해서 코드를 수정한 뒤, 다시 테스트를 실행하려면 JUnit 테스트 뷰의 녹색 Rerun Test 버튼을 클릭하면 된다. 또, 테스트 목록에서 테스트 클래스나 테스트 메소드를 더블클릭하면 해당 코드를 편집기에 보여준다.
JUnit은 한 번에 여러 테스트 클래스를 동시에 실행할 수도 있다. 소스 트리에서 특정 패키지를 선택하고 컨텍스트 메뉴의 Run As > JUnit Test를 실행하면, 해당 패키지 아래에 있는 모든 JUnit 테스트를 한 번에 실행해준다.
빌드 툴
프로젝트의 빌드를 위해 ANT나 메이븐(Maven)같은 빌드 툴과 스크립트를 사용하고 있다면, 빌드 툴에서 제공하는 JUnit 플러그인이나 태스크를 이용해 JUnit 테스트를 실행할 수 있다. 테스트 실행 옵션에 따라서 HTML이나 텍스트 파일의 형태로 보기 좋게 만들어진다.
테스트 결과의 일관성
반복적으로 테스트를 했을 때 테스트가 실패하기도 하고 성공하기도 한다면 이는 좋은 테스트라고 할 수가 없다. 코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.
UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다는 점이다. 가장 좋은 해결책은 addAndGet()테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서, 테스트를 수행하기 이전 상태로 만들어주는 것이다.
deleteAll()와 getCount()추가
- deleteAll(): USER 테이블의 모든 레코드를 삭제한다.
- getCount(): USER 테이블의 레코드 개수를 돌려준다. ``` @Test public void addAndGet() throws SQLException { …
dao.deleteAll(); assertThat(dao.getCount(), is(0);
User user = new User(); user.setId(“gyumee”); user.setName(“박성철”); user.setPassword(“springno1”);
dao.add(user); assertThat(dao.getCount(), is(1));
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName())); assertThat(user2.getPassword(), is(user.getPassword())); }
#### 포괄적인 테스트
###### getCount() 테스트
getCount()에 대한 좀 더 꼼꼼하게 테스트 해야 한다. 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. JUnit은 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다.
시나리오
* USER 테이블의 데이터를 모두 지우고 getCount()로 레코드 개수가 0임을 확인한다.
* 3개의 사용자 정보를 하나씩 추가하면서 매번 getCount()의 결과가 하나씩 증가하는지 확인한다.
> 주의해야 할 점은 두개의 테스트가 어떤 순서로 실행될지는 알 수 없다. JUnit은 특정한 테스트 메소드의 실행 순서를 보장해주지 않는다. 테스트의 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못만든 것이다. 모든 테스트는 실행 순서에 상관없이 독립적으로 항상 동일한 결과를 낼 수 있도록 해야 한다.
###### get() 예외조건에 대한 테스트
get() 메소드에 전달된 id값에 해당하는 사용자 정보가 없다면? null과 같은 특별한 값을 리턴하거나, id에 해당하는 정보를 찾을 수 없다고 예외를 던지는 것이다. 예외를 던져보자.
일반적으로는 테스트 중에 예외가 던져지면 테스트 메소드의 실행은 중단되고 테스트는 실패한다. assertThat()을 통한 검증 실패는 아니고 테스트 에러라고 볼 수 있다. 그런데 이번에는 반대로 테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이고, 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다.
@Test(expected = EmptyResultDataAccessException.class) // 테스트 중에 발생할 것으로 기대하는 예외 클래스를 지정해준다. public void getUserFailure() throws SQLException { ApplicationContext context = new GenericXmlApplicationContext (“applicationContext.xml”);
UserDao dao = context.getBean(“userDao”, UserDao.class); dao.deleteAll(); assertThat(dao.getCount(), is(0));
dao.get(“unknown_id); // 이 메소드 실행 중에 예외가 발생해야 한다. 예외가 발생하지 않으면 테스트가 실패한다. }
> @Test에 expected를 추가해놓으면 보통의 테스트와는 반대로, 정상적으로 테스트 메소드를 마치면 테스트가 실패하고, expected에서 지정한 예외가 던져지면 테스트가 성공한다. 예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 쓸 수 있다.
###### 포괄적인 테스트
"항상 네거티브 테스트를 먼저 만들라"
개발자는 빨리 테스트를 만들어 성공하는 것을 보고 다음 기능으로 나아가고 싶어하기 때문에, 긍정적인 경우를 골라서 성공할 만한 테스트를 먼저 작성하게 된다. 그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다.
#### 테스트가 이끄는 개발
###### 기능설계를 위한 테스트
추가하고 싶은 기능을 일반 언어가 아니라 테스트 코드로 표현해서, 마치 코드로 된 설계문서처럼 만들고 나면, 바로 이 테스트를 실행해서 설계한 대로 코드가 동작하는지를 빠르게 검증할 수 있다.
###### 테스트 주도 개발
만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발방법이 있다. 이를 **테스트 주도 개발**(TDD, Test Driven Development)이라고 한다. 또는 개발자가 테스트를 코드보다 먼저 작성한다고 해서 테스트 우선 개발이라고도 한다.
#### 테스트 코드 개선
기계적으로 반복되는 부분
ApplicationContext context = new GenericXmlApplicationContext(“applicationContext.xml”); User dao = context.getBean(“userDao”, UserDao.class);
###### @Before
* 중복됐던 코드를 넣을 setUp()이라는 이름의 메소드를 만들고 테스트 메소드에서 제거한 코드를 넣는다.
* 로컬 변수인 dao를 테스트 메소드에서 접근할 수 있도록 인스턴스 변수로 변경한다.
* setUp() 메소드에 @Before라는 애노테이션을 추가해준다.
JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식
1. 테스트 클래스에서 @Test가 붙은 pulbic이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
2. 테스트 클래스의 오브젝트를 하나 만든다.
3. @Before가 붙은 메소드가 있으면 실행한다.
4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
5. @After가 붙은 메소드가 있으면 실행한다.
6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
7. 모든 테스트의 결과를 종합해서 돌려준다.
> @Before나 @After 메소드를 테스트 메소드에서 직접 호출하지 않기 때문에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다.
> 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다. 한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다. 테스트 클래스가 @Test 테스트 메소드를 두 개 갖고 있다면, 테스트가 실행되는 중에 JUnit은 이 클래스의 오브젝트를 두 번 만들 것이다. JUnit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만든다. 덕분에 인스턴스 변수도 부담 없이 사용할 수 있다.
> 테스트 메소드의 일부에서만 공통적으로 사용되는 코드가 있다면 @Before보다는 일반적인 메소드 추출 방법을 써서 메소드를 분리하고 테스트 메소드에서 직접 호출해 사용하도록 만든다. 아니면 아예 공통적인 특징을 지닌 테스트 메소드를 모아서 별도의 테스트 클래스로 만드는 방법도 있다.
###### 픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before 메소드를 이용해 생성해 두면 편리하다.
<hr/>
### 스프링 테스트 적용
@Before 메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션 컨텍스트도 그 개수만큼 만들어진다. 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다. JUnit은 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다. 이 메소드에서 애플리케이션 컨텍스트를 만들어 스태틱 변수에 저장해두고 테스트 메소드에서 사용하게 할 수 도 있지만, 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 더 편리하다.
#### 테스트를 위한 애플리케이션 컨텍스트 관리
스프링은 JUint을 이용하는 테스트 컨텍스트 프레임워크를 제공한다. 간단한 애노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할수 있다.
###### 스프링 테스트 컨텍스트 프레임워크 적용
@RunWith(SpringJUnit4ClassRunner.class) // 스프링의 테스트 컨텍스트 프레임워크의 JUnit 확장기능 지정 @ContextConfiguration(locations=”/applicationContext.xml”) // 테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치 지정 public class UserDaoTest { @Autowired private ApplicationContext context; // 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다. …
@Before public void setUp() { this.dao = this.context.getBean(“userDao”, UserDao.class); … } }
@RunWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다. SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치를 지정한 것이다.
###### 테스트 메소드의 컨텍스트 공유
하나의 애플리케이션 컨텍스트가 만들어져 모든 테스트 메소드에서 사용된다. 테스트가 실행되기 전에 딱 한번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다. 첫 번째 테스트가 실행될 때 최초로 애플리케이션 컨텍스트가 처음 만들어지면서 가장 오랜 시간이 소모되고, 그 다음부터는 이미 만들어진 애플리케이션 컨텍스트를 재사용할 수 있기 때문에 테스트 실행 시간이 매우 짧아진다.
###### 테스트 클래스의 컨텍스트 공유
여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.
@ContextConfiguration(locations=”/applicationContext.xml”) // 이 부분이 같다면
> 수백 개의 테스트 클래스를 만들었는데 모두 같은 설정파일을 사용한다고 해도 테스트 전체에 걸쳐 단 한개의 애플리케이션 컨텍스트만 만들어 사용할 수 있다.
> 스프링은 설정파일의 종류만큼 애플리케이션 컨텍스트를 만들고, 같은 설정파일을 지정한 테스트에서는 이를 공유하게 해준다.
###### @Autowired
@Autowired는 스프링의 DI에 사용되는 특별한 애노테이션이다. 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾아 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져올 수 있는데, 이런 방법을 타입에 의한 자동와이어링이라고 한다.
> 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 셈이고 DI도 가능하다.
#### DI와 테스트
UserDao와 DB 커넥션 생성 클래스 사이에는 DataSource라는 인터페이스를 뒀다. 그래서 UserDao는 자신이 사용하는 오브젝트의 클래스가 무엇인지 알 필요가 없다. 또한 DI를 통해 외부에서 사용할 오브젝트를 주입받기 때문에 오브젝트 생성에 대한 부담을 지지 않아도 된다. 코드의 수정 없이도 얼마든지 의존 오브젝트를 바꺼가며 사용할 수 있다.
인터페이스를 두고 DI를 적용하는 이유
* 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문
* 클래스의 구현 방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있다. 새로운 기능을 넣기 위해 기존 코드는 전혀 수정할 필요도 없다. 추가했던 기능이 필요 없어지면 언제든지 설정파일을 간단히 수정해서 제거해버릴 수도 있다.
* 효율적인 테스트를 위해서다. 테스트를 잘 활용하려면 자동으로 실행 가능하며 빠르게 동작하도록 테스트 코드를 만들어야 한다. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는 데 중요한 역할을 한다.
###### 테스트 코드에 의한 DI
테스트용 DB에 연결해주는 DataSource를 테스트 내에서 직접 만들 수 있다.(테스트 시 운영용을 사용했다가 큰 사고가 날 수 있다.)
@DirtiesContext // 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에 알려준다. public class UserText { @Autowired UserDao dao;
@Before public void setUp() { … DataSource dataSource = new SingleConnectionDataSource(“jdbc:mysql://localhost/testdb”, “spring”, “book”, true); // 테스트에서 UserDao가 사용할 DataSource 오브젝트를 직접 생성한다. dao.setDataSource(dataSource); // 코드에 의한 수동 DI } }
> 이미 애플리케이션 컨텍스트에서 applicationContext.xml 파일의 설정정보를 따라 구성한 오브젝트를 가져와 의존관계를 강제로 변경했기 때문에 굉장히 주의해서 사용해야 한다.
###### @DirtiesContext
스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태를 변경한다는 것을 알려준다. 테스트 컨텍스트는 이 애노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않는다. 테스트 메소드를 수행하고 나면 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다. 테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서다.
> 애플리케이션 컨텍스트를 매번 만드는 건 조금 찜찜하다.
※ 하나의 메소드 에서만 컨텍스트 상태를 변경한다면 메소드 레벨에 이 애노테이션을 붙여줄 수 있다. 해당 메소드의 실행이 끝나고 나면 이후에 진행되는 테스트를 위해 변경된 애플리케이션 컨텍스트는 폐기되고 새로운 애플리케이션 컨텍스트가 만들어진다.
###### 테스트를 위한 별도의 DI 설정
아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다. 즉 두 가지 종류의 설정파일을 만들어서 하나에는 서버에서 운영용으로 사용할 DataSource를 빈으로 등록해두고, 다른 하나에는 테스트에 적합하게 준비된 DB를 사용하는 가벼운 DataSource가 빈으로 등록되게 만드는 것이다. 그리고 테스트에서는 항상 테스트 전용 설정파일만 사용하게 해주면 된다.<br/>
> 번거롭게 수동 DI 하는 코드나 @DirtiesContext도 필요 없다.
###### 컨테이너 없는 DI 테스트
테스트 코드에서 직접 오브젝트를 만들고 DI해서 사용할 수 있다.
public class UserDaoTest { UserDao dao; // @Autowired가 없다. …
@Before public void setUp() { // 오브젝트 생성, 관계설정 등을 모두 직접 해준다. … dao = new UserDao(); DataSource dataSource = new SingleConnectionDataSource(“jdbc:mysql://localhost/testdb”, “spring”, “book”, true); dao.setDataSource(dataSource); } }
> DI는 객체지향 프로그래밍 스타일이다. 따라서 DI를 위해 컨테이너가 반드시 필요한 것은 아니다. DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.
###### DI를 이용한 테스트 방법 선택
* 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
* 여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우 스프링의 설정을 이용한 DI 방식의 테스트를 이용하면 편리하다. 테스트에서 애플리케이션 컨텍스트를 사용하는 경우에는 테스트 전용 설정 파일을 따로 만들어 사용하는 편이 좋다.
* 테스트 설정을 따로 만들었다고 하더라도 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우 컨텍스트에서 DI 받은 오브젝트에 다시 테스트 코드로 수동 DI 해서 테스트하는 방법을 사용하면 된다. 테스트 메소드나 클래스에 @DirtiesContext 애노테이션을 붙이는 것을 잊지 말자.
<hr/>
### 학습 테스트로 배우는 스프링
자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대해서도 테스트를 작성해야 한다. 이런 테스트를 학습 테스트라고 한다. 학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히려는 것이다. 따라서 테스트이지만 프레임워크나 기능에 대한 검증이 목적이 아니다.
#### 학습 테스트의 장점
* 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
* 학습 테스트 코드를 개발 중에 참고할 수 있다.
* 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
* 테스트 작성에 대한 좋은 훈련이 된다.
* 새로운 기술을 공부하는 과정이 즐거워진다.
> 학습 테스트는 당장 적용할 일부 기능의 사용법을 익히기 위해서만이 아니라 새로운 프레임워크나 기술을 전반적으로 공부하는 과정에서도 유용하다.
#### 한 걸음 더
* not(): 뒤에 나오는 결과를 부정하는 매처다.
* is()는 equals() 비교를 해서 같으면 성공이지만 is(not())은 반대로 같지 않아야 성공한다.
* sameInstance(): 실제로 같은 오브젝트인지를 비교한다.(동일성 비교 매처)
* hasItem(): 컬렌션의 원소인지를 검사하는 매처.
* assertTrue(): assertThat()방식보다 간결해진다.
assertThat(contextObject == null || contextObject == this.context, is(true)); // is()는 타입만 일치하면 어떤 값이든 검증할 수 있다.
assertTrue(contextObject == null || contextObject == this.context);
* either(): 뒤에 이어서 나오는 or()와 함께 두 개의 매처의 결과를 OR 조건으로 비교해준다. 두 가지 매처 중 하나만 true로 나와도 성공이다.
* nullValue(): 오브젝트가 null인지를 확인해준다.
assertThat(contextObject, either(is(nullValue())).or(is(this.context))); ```
버그 테스트
오류가 발견됐을 때 무턱대고 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어 보는 편이 유용하다.
버그 테스트는 일단 실패하도록 만들어야 한다. 버그가 원인이 되서 테스트가 실패하는 코드를 만드는 것이다. 그리고 나서 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다. 테스트가 성공하면 버그는 해결된 것이다.
장점
- 테스트의 완성도를 높여준다.
- 버그의 내용을 명확하게 분석하게 해준다.
- 기술적인 문제를 해결하는 데 도움이 된다.
정리
- 테스트는 자동화돼야 하고, 빠르게 실행할 수 있어야 한다.
- main() 테스트 대신 JUnit 프레임워크를 이용한 테스트 작성이 편리하다.
- 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안 된다.
- 테스트는 포괄적으로 작성해야 한다. 충분한 검증을 하지 않는 테스트는 없는 것보다 나쁠 수 있다.
- 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.
- 테스트하기 쉬운 코드가 좋은 코드다.
- 테스트를 먼저 만들고 테스트를 성공시키는 코드를 만들어가는 테스트 주도 개발 방법도 유용하다.
- 테스트 코드도 애플리케이션 코드와 마찬가지로 적절한 리팩토링이 필요하다.
- @Before, @After를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.
- 스프링 테스트 컨텍스트 프레임워크를 이용하면 테스트 성능을 향상시킬 수 있다.
- 동일한 설정파일을 사용하는 테스트는 하나의 애플리케이션 컨텍스트를 공유한다.
- @Autowired를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI 할 수 있다.
- 기술의 사용 방법을 익히고 이해를 돕기 위해 학습 테스트를 작성하다.
- 오류가 발견될 경우 그에 대한 버그 테스트를 만들어두면 유용하다.
JUnit은 테스트 메소드를 수행할 때마다 새로운 오브젝트를 만든다.
JUnit과 반대로 스프링의 테스트용 애플리케이션 컨텍스트는 테스트 개수에 상관없이 한 개만 만들어진다. 또 이렇게 만들어진 컨텍스트는 모든 테스트에서 공유된다.
출처 : https://github.com/Masssidev/toby-vol1