2.1 UserDaoTest
테스트란 결국 내가 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인해서 만든 코드를 확신할 수 있게 해주는 작업이다.
보통 웹 프로그램에서 사용하는 DAO를 테스트하는 방법은 다음과 같다. DAO를 만든 뒤 바로 테스트하지 않고 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 코드로 만들고 이를 테스트용 서버에 배치한뒤 웹 화면을 통해 값을 입력하고 버튼을 눌러 등록한다.
→ DAO뿐만 아니라 서비스 클래스, 컨트롤러, JSP 뷰 등 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다는 점이 가장 큰 문제점이다
- 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
- UserDaoTest는 한 가지 관심에 집중할 수 있게 작은 단위로 만들어진 Test이다.
- IDE나 도스창에서도 테스트 수행이 가능하다.
- 이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트라고 한다.
- 일반적으로 단위는 작을수록 좋다.
- 외부의 리소스에 의존하는 테스트는 단위테스트가 아니라고 보기도 하는 것이다.
- 단위 테스트를 하는 이유는
- 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서이다.
- 확인의 대상과 조건이 간단하고 명확할수록 좋다.
UserDaoTest는 테스트할 데이터가 자동으로 코드를 통해서 제공되고 테스트 작업 역시 코드를 통해 자동으로 실행된다는 특징을 가지고 있다.
테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
UserDaoTest처럼 애플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보다는 별도로 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫다.

클래스를 분리하고 유연한 설계구조로 발전시키면서 테스트 코드를 넣을 위치를 결정하기가 애매하기때문이다.
UserDao를 개발하면서 작은 단계를 거치는 동안 테스트를 수행해서 확인을 가지고 코드를 변경해갔기 때문에 전체적으로 코드를 개선하는 작업에 속도가 붙고 더 쉬워졌다.
테스트를 이용하면 새로운 기능도 기대한 대로 동작하는지 확인할 수 있을 뿐 아니라 기존에 만들어뒀던 기능들이 새로운 기능을 추가하느라 수정한 코드에 영향을 받지 않고 여전히 잘 동작하는지 확인할 수도 있다.
UserDaoTest는 다음과 같은 문제점이 존재한다.
- 수동 확인 작업의 번거로움UserDaoTest는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다. 하지만 여전히 사람의 눈으로 결과를 확인하는 과정이 필요하다. 만약 테스트하는 값이 많아지고 복잡해진다면 불편함을 느낄수 밖에 없다.
- 실행 작업의 번거로움기능이 많아진다면 많아진 기능만큼 테스트 하는 코드가 담긴 main()메소드를 수행해야 한다. 또한 그 결과를 정리하는 것 역시 번거롭다.
2.2 UserDaoTest 개선
모든 테스트는 성공과 실패의 두가지 결과를 가질수 있다. 그리고 실패의 경우 에러가 발생해서 실패하는 경우와 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나오는 경우로 구분해볼 수 있다.
기존 애플리케이션 코드를 부담없이 수정하고 새로 도입한 기술의 적용에 문제가 없는지 확인할 수 있는 가장 좋은 방법은 빠르게 실행가능하고 스스로 테스트 수행과 기대하는 결과에 대한 확인까지 해주는 코드로 된 자동화된 테스트를 만들어두는 것이다.
위의 단순한 main() 메소드로는 한계가 있다.
- 일정한 패턴을 가진 테스트를 볼 수 없다.
- 많은 테스트를 실행시키기 어렵다.
- 테스트 결과를 종합해서 볼 수 없다.
- 테스트가 실패한 곳을 빠르게 찾을 수 없다.
기존에 만들었던 main() 메소드 테스트는 Junit(혹은 AssertJ)프레임워크에 적용하기엔 적합하지 않다. 테스트가 main() 메소드로 만들어졌다는 건 제어권을 직접 갖는다는 의미이기때문이다.
Junit프레임 워크가 요구하는 조건은 두가지이다.
- 메소드가
Public
으로 선언되어야 하고
- 다른 하나는
@Test
라는 어노테이션이 붙어야 한다는 것
- 반환 타입이 void이면 된다.
main() 대신 일반 메소드로 만들고 적절한 이름(테스트의 의도가 무엇인지를 알 수 있는 이름)을 붙여준다.
➕Junit과 AssertJ의 차이점
- 정의
- Junit: 자바에 구축된 자동화 테스트가 가능한 프레임워크
- AssertJ: 자바 테스트에서 유창하고 풍부한 assertions를 작성하는데 사용되는 오픈소스 라이브러리
- Junit말고 AssertJ를 사용하는 이유
- 배우기 쉽다.
- 공식 문서가 잘 정리되어 있기 때문이다.
- 사용하기 쉽다.
- 테스트 클래스에 dependency 및 static import만 추가해 사용하면 된다.
- 다양한 Assertions메소드를 지원한다.
- 코드의 가독성이 올라간다.
- 메서드 체이닝을 지원한다.
- 자동완성을 지원하기 때문에 모든 메서드 이름을 기억하지 않아도 된다.
- 배우기 쉽다.
기존의 UserDaoTest
if(!user.getName().equals(user2.getName())) {...}
Junit의 assertThat이라는 스태틱 메소드를 이용해 변경한 코드
assertThat(user2.getName(), is(user.getName()))
AssertJ를 이용해서 변경한 코드
assertThat(user2.getName()).isEqualTo(user.getName());
assertThat()
메소드는 첫번째 파라미터
의 값을 뒤에 나오는 매처(matcher, is()가 매처 역할을 한다.)
라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고 아니면 테스트가 실패하도록 만들어준다.
2.3 개발자를 위한 테스팅 프레임워크 Junit
스프링으로 개발을 하면서 Junit 보다는 AssertJ를 더 많이 사용하기에 AssertJ를 이용한 사용 방법을 작성했다.
AssertJ를 사용하기 위해 Maven과 Gradle 둘다 사용가능하다.
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.24.2</version>
<scope>test</scope>
</dependency>
testImplementation 'org.assertj:assertj-core:3.24.2'
이후 사용하려고 하는 테스트 클래스에서 다음과 같이 import하면 된다.
import static org.assertj.core.api.Assertions.*;
UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다는 점이다.
코드에 변경사항이 없다면 테스트는 항상 동일한 결과를 내야 한다.
그리고 가장 좋은 해결책은 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서 테스트를 수행하기 이전 상태로 만들어주는 것이다.
이는 UserDao에 모든 값을 삭제하는 메소드를 추가하고 이를 호출해주는 방식으로 가능하다.
public void deleteAll() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("delete from users");
ps.executeUpdate();
ps.close();
c.close();
}
그리고 현재 테이블에 몇개의 레코드가 있는지 확인해주는 코드를 추가한다.
public int getCount() throws SQLException {
Connection c = dataSource.getConnection();
PreparedStatement ps = c.prepareStatement("select count(*) from users");
ResultSet rs = ps.executeQuery();
rs.next();
int count = rs.getInt(1);
rs.close();
ps.close();
c.close();
return count;
}
새로운 기능을 추가했으니 추가된 기능에 대한 테스트도 만들어야 한다.
새로운 기능(deleteAll
, getCount
)이 올바르게 동작하는지 확인하기 위해 user
테이블에 수동으로 데이터를 넣고 deleteAll
을 실행한 뒤에 테이블에 남은게 있는지 확인해야 하는데 이는 사람이 테스트 과정을 참여해야 하므로 자동화가 아닌 뿐 더러 반복적으로 실행한 테스트방법 역시 아니다.
가장 간단한 방법은 해당 방법을 기존의 addAndGet()
테스트에 추가한 것이다.
@Test
public void andAndGet() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
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()));
}
단위 테스트는 항상 일관성 있는 결과가 보장돼야 한다. 또한 DB에 나아 있는 데이터와 같은 외부 환경에 영향을 받지 말아야 하는 것은 물론이고 테스트를 실행하는 순서를 바꿔도 동일한 결과가 보장되도록 만들어야 한다.
미처 생각하지 못한 문제가 숨어 있을지도 모르니 더 꼼꼼한 테스트를 해보는 것이 좋은 자세다 또 한 가지 결과만 검증하고 마는 것은 상당히 위험하다.
테스트 메소드는 한번에 한 가지 검증 목적에만 충실한 것이 좋다.
테스트를 할때 주의해야 할 점은 테스트의 실행 순서를 보장해주지 않는다는 것이다.
(Junit5에서는 @TestMethodOrder
를 통해서 테스트 메소드 실행순서를 정할 수 있다.)
만약 DB에 원하는 ID의 사용자 정보가 없을때는
- null과 같은 특별한 값을 반환하거나
- 예외를 던지는 것이다.
여기서는 예외를 던지는 것에 대해 알아본다.
일반적인 Junit(AssertJ)의 @Test
어노테이션은 예외를 던지지 않고 정상적으로 작업을 마치면 테스트가 성공했다고 판단한다. 그러나 이번에는 원하는 예외를 던져야 정상이다.
그러나 예외 발생 여부는 메소드를 실행해서 리턴 값을 비교하는 방법으로 확인할 수 없다는 점이다.
즉 assertThat() 메소드로는 예외 발생에 대한 검증이 불가능하다.
그렇기에 다음과 같은 방법으로 예외 발생에 대한 검증을 해야한다.
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
dao.deleteAll();
assertThat(dao.getCount(), is(0));
dao.get("unknown_id");
}
@Test
어노테이션의 expected 엘리먼트로 발생하리라 기대하는 예외 클래스를 넣어주면 된다.
- 테스트가 성공한다면 원하는 기능을 가진 코드가 제대로 만들어졌다고 보면 된다.
- 개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공하는 테스트만 골라서 만드는 것이다. (정상 플로우)
- 그렇기에 다양한 경우 대한 전문적인 테스트가 수행될 필요가 있다.
- 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는게 좋다.
- 추가하고 싶은 기능을 일반 언어(자연어)가 아니라테스트 코드로 표현할 경우 마치 코드로 된 설계문서와 다름 없다.
- 만약 테스트가 실패하면 이때는 설계한 대로 코드가 만들어지지 않았음을 바로 알 수 있다. 그리고 결국 테스트가 성공한다면 그 순간 코드 구현과 테스트라는 두 가지 작업이 동시에 끝나는 것이다.
- 테스트 코드를 먼저 만들고 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방법을 테스트 주도 개발 (TDD, Test Driven Development)라고 한다.
“실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다.”
는 TDD의 기본 원칙이다.
- 개발자들이 정신없이 개발을 하다 보면 사이사이 테스트를 만들어서 코드를 점검할 타이밍을 놓치는 경우가 많다. 그러나 TDD는 아예 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
- TDD에서는 테스트 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 한 짧게 가져가도록 권장한다.
- TDD를 하면 자연스럽게 단위 테스트를 만들 수 있다.
- 테스트는 코드를 작성한 후에 가능한 빨리 실행할 수 있어야 한다. 그러려면 테스트 없이 한번에 너무 많은 코드를 만드는 것은 좋지 않다.
- 일정분량 코딩을 먼저 해놓고 빠른 시간안에 테스트코드를 만들어 테스트해도 상관없다.
- 테스트를 만들고 자주 실행하여도 개발속도에 영향을 미치지 않는다. 그러한 이유로는 테스트는 애플리케이션 코드보다 상대적으로 작성하기 쉬운데다 각 테스트가 독립적이기 때문에 코드의 양에 비해 작성하는 시간은 얼마 걸리지 않는다. 게다가 테스트 덕분에 오류를 빨리 잡아낼 수 있어서 전체적인 개발 속도는오히려 빨라진다.
테스트 코드도 언제든지 내부구조와 설꼐를 개선해서 좀 더 깔끔하고 이해하기 쉬우면 변경이 용이한 코드로 만들 필요가 있다.
Junit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
- 테스트 클래스에서
@Test
가 붙은public
이고void
형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
- 테스트 클래스의 오브젝트를 하나 만든다.
@Before
가 붙은 메소드가 있으면 실행한다.
@Test
가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다.
@After
가 붙은 메소드가 있으면 실행한다.
- 나머지 테스트 메소드에 대해 2~5번을 반복한다.
- 모든 테스트의 결과를 종합해서 돌려준다.
- 테스트 메소드 간에 서로 주고받을 정보나 오브젝트가 있다면 인스턴스 변수를 이용해야 한다.
- 각 테스트 메소드를 실행할 때 만다 오브젝트는 하나의 테스트 메소드를 사용하고나면 버려진다.
- Junit 개발자는 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장해주기 위해 매번 새로운 오브젝트를 만들게 했다.
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스쳐라고 한다.
ex) UserDaoTest에서 dao가 대표적인 픽스처다.
2.4 스프링 테스트 적용
테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
- 간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.
@RunWith
는 Junit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다. SpringJunit4ClassRunner라는 Junit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 애플리케이션 컨택스트를 만들고 관리하는 작업을 진행해준다.
@ContextConfiguration
은 자동으로 만들어줄 애플리케이션컨텍스트의 설정파일 위치를 지정한 것이다
➕@RunWith
어노테이션은 Junit5에서는 생략 가능
스프링의 JUnit 확장 기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만들어두고, 테스트 오브젝트가 만들어질 때마다 특별한 방법을 이용해 애플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것이다.
스프링은 테스트 클래스 안에서 애플리케이션 컨텍스트를 공유하게 해주는 것 뿐만 아니라 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다.(같은 설정파일을 사용할 경우)
@Autowired
가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾아서 주입해준다.
이전의 UserDaoTest
에서 ApplicationContext.xml
에 ApplicationContext
타입의 빈에 대해서 정의하지 않았지만 @Autowired
로 빈을 주입받았다. 이는 스프링 어플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록하기 때문이다.
@Autowired
는 타입으로 가져올 빈 하나를 선택할 수 없는 경우에는 변수의 이름과 같은 이름의 빈이 있는지 확인한다. 만약 동일한 타입의 빈이 두개 이상 등록되어 있을 경우 변수이름으로 빈을 찾는다. 이때 변수이름으로도 빈을 찾을 수 없는 경우에는 예외가 발생한다.
만약 특정 타입의 클래스가 변하지 않는다고 하더라고 인터페이스를 두고 DI를 적용해야 한다.
그러한 이유로는 다음과 같다.
- SW개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
- 클래스의 구현방식은 바뀌지 않는다고 하더라도 인터페이스를 두고 DI를 적용하게 해두면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
- 효율적인 테스트를 손쉽게 만들기 위해서라도 DI를 적용해야 한다.
테스트 코드 내에서 직접 DI를 해도 된다.
@DirtiesContext
는 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨택스트의 상태를 변경한다는 것을 알려준다. 즉 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트를 공유하지 않는다.
스프링 컨테이너없이 테스트할수도 있으면 스프링 컨테이너가 없을 경우 테스트 수행속도가 가장 빠르고 테스트 자체가 간결하다.
여러 오브젝트와 복잡한 의존관계를 갖고 있는 오브젝트를 테스트해야 할 경우가 있다. 이때는 스프링의 설정을 이용한 DI방식의 테스트를 이용하면 편리하다.
2.5 학습 테스트로 배우는 스프링
때로는 자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한라이브러리 등에 대해서도 테스트를 작성해야 한다. 그리고 이를 학습 테스트라고한다.
학습 테스트의 목적은 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용방법을 익히려는 것이다.
학습테스트의 장점은 다음과 같다.
- 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
- 학습 테스트 코드를 개발 중에 참고할 수 있다.
- 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
- 테스트 작성에 대한 좋은 훈련이 된다.
- 새로운 기술을 공부하는 과정이 즐거워진다.
학습테스트는 당장 적용할 일부 기능의 사용법을 익히기 위해서만이 아니라 새로운 프레임워크나 기술을 전반적으로 공부하는 과정에서도 유용하다.
버그테스트란 코드에 오류가 있을때 그 오류를 가장 잘 드러내줄 수 있는 테스트를 말한다.
버그테스트는 일단 실패하도록 만들어야 한다. 버그가 원인이 되서 테스트가 실해하는 코드를 만드는 것이다.
버그테스트의 장점은 다음과 같다.
- 테스트의 완성도를 높여준다.
- 버그의 내용을 명확하게 분석하게 해준다.
- 기술적인 문제를 해결하는데 도움이 된다.
추가
아래는 "2장 테스트" 내용으로 스터디를 진행하면서 나온 이야기를 따로 간추린 것입니다.
- 객체를 스프링의 빈으로 생성하는 것과 스프링을 이용하지 않고 static으로 생성하는 것의 차이는 무엇인가요?
- 스프링 빈은 어플리케이션 컨텍스트이 싱글톤 패턴을 이용해서 하나의 객체만 존재하는 것을 보장해주고 static 키워드가 붙은 변수 역시 JVM에 하나만 있다는 점에서는 유사하지만 static 키워드가 붙은 변수는 스프링이 제공하는 라이프 사이클 관리, 의존성주입, 테스트 용이성, 스코프 관리, AOP 및 다른 스프링기능 활용을 하지 못한다는 점이 차이점이며
- 어플리케이션 컨텍스트와 빈은 JVM의 힙영역에 생성된다는 점과 static 키워드는 JVM의 정적 영역에 생성된다는 점이 다릅니다.
- Junit과 AssertJ는 하나의 클래스에서 동시에 사용할 수 없나요?
- 테스트 코드를 작성할때 Junit5로 전체적인 테스트 코드를 작성하되 검증하는 부분(Assertions)관련 부분은 assertJ의 코드를 사용할수 있습니다.
'책 > 토비의 스프링 3.1(1권, 완)' 카테고리의 다른 글
6장 AOP (2) | 2023.11.23 |
---|---|
5장 서비스 추상화 (1) | 2023.11.23 |
4장 예외 (1) | 2023.11.02 |
3장 템플릿 (0) | 2023.10.12 |
1장 오브젝트와 의존관계 (0) | 2023.09.27 |