티스토리 뷰
작은 단위의 테스트가 필요한 이유
- 한꺼번에 너무 많은 것을 몰아서 테스트하면 테스트 수행 과정도 복잡해지고, 오류가 발생했을 때 정확한 원인을 찾기가 힘들어진다. (e.g. 웹화면을 이용한 테스트) 따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
- 개발자가 설계하고 만든 코드가 원래 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위함. 다른 사람에 의해 테스트될 때보다 빠르게 오류 수정 가능.
UserDaoTest 특징
- 자동 수행 테스트 코드
- 테스트할 데이터가 코드를 통해 제공되고, 테스트 작업도 코드를 통해 자동으로 실행됨. 개발자는 main 메소드를 실행하기만 하면 됨
- 점진적인 개발을 위한 테스트
- 기능 추가, 코드 개선할 때 이전 기능이 영향받지 않는지 확인 가능
UserDaoTest 문제점
- 수동 확인 작업의 번거로움
- 테스트 output - 기대 output 비교를 사람이 직접 해야 함
- 실행 작업의 번거로움
- main() 메소드를 일일이 실행시켜야 함
문제점 해결 방안
- 테스트 검증의 자동화
if (!user.getName().equals(user2.getName())) {
System.out.println("테스트 실패");
} else if (!user.getPassword().equals(user2.getPassWord())) {
System.out.println("테스트 실패");
} else {
System.out.println("테스트 성공");
}
- 테스트 효율적인 수행과 결과 관리 : JUnit 테스트로 전환
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
User user = new User();
user.setId("abcd");
user.setName("sb");
user.setPassword("123");
userDao.add(user);
User user2 = userDao.get(user.getId());
Assert.assertThat(user2.getName(), CoreMatchers.is(user.getName()));
}
}
테스트 결과의 일관성
- 단위테스트는 코드가 바뀌지 않는다면 매번 실행할 때마다 동일한 테스트 결과를 얻을 수 있어야 한다.
- UserDaoTest 실행 전에 DB 의 USER 테이블 데이터를 모두 삭제해줘야 함.
- 해결책은, 테스트를 마치고 나면 DB 데이터를 삭제해서 테스트를 수행하기 이전 상태로 만드는 것.
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
User user = new User();
user.setId("abcd");
user.setName("sb");
user.setPassword("123");
userDao.add(user);
assertThat(userDao.getCount(), is(1));
User user2 = userDao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
}
}
- 더 꼼꼼한 테스트를 해보는 것이 좋은 자세다. 테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다. 특히 한 가지 결과만 검증하고 마는 것은 상당히 위험하다. 이런 테스트는 마치 하루에 두 번은 정확히 맞는다는 시계와 같을 수도 있다. 죽은 시계 말이다.
예외 조건에 대한 테스트
@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException, ClassNotFoundException {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
UserDao userDao = context.getBean("userDao", UserDao.class);
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
userDao.get("unknown"); //예외 발생
}
- EmptyResultDataAccessException 예외처리를 하지 않았기 때문에 위 테스트는 실패한다. 테스트가 통과하도록 get() 을 수정해보자.
public User get(String id) throws EmptyResultDataAccessException, SQLException {
Connection connection = getConnection();
PreparedStatement ps = connection.prepareStatement(
"select * from users where id = ?"
);
ps.setString(1, id);
ResultSet rs = ps.executeQuery();
User user = null;
if (rs.next()) {
user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
}
rs.close();
ps.close();
connection.close();
if (user == null) {
throw new EmptyResultDataAccessException();
}
return user;
}
테스트 코드 작성시 주의사항
- 개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공하는 케이스만 골라서 만드는 것이다.
- 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. “항상 네거티브 테스트를 먼저 만들라”
- 코드를 만들고 나서 시간이 많이 지나면 테스트를 만들기가 귀찮아진다. … 결국 테스트 작성은 자꾸 뒷전으로 밀려나거나 점점 더 성의 없는 테스트를 만들게 될지도 모른다.
TDD 장점
- 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있다.
- 코드를 만들어 테스트를 실행하는 그 사이의 간격이 매우 짧다. 개발한 코드의 오류는 빨리 발견할수록 좋다. … 테스트 없이 오랜 시간 동안 코드를 만들고 나서 테스트를 하면, 오류가 발생했을 때 원인을 찾기가 쉽지 않다.
테스트 코드 개선
- 중복 제거
public class UserDaoTest {
private UserDao userDao;
@Before
public void setUp() {
ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
this.userDao = context.getBean("userDao", UserDao.class);
}
@Test
public void addAndGet() throws SQLException, ClassNotFoundException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
User user = new User();
user.setId("abcd");
user.setName("sb");
user.setPassword("123");
userDao.add(user);
assertThat(userDao.getCount(), is(1));
User user2 = userDao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
}
@Test(expected = EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException, ClassNotFoundException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
userDao.get("unknown"); //예외 발생
}
}
JUnit 이 테스트를 수행하는 과정
- 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만든다.
- why? : 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 확실히 보장하기 위해서.
테스트를 위한 applicationContext 관리
- 이전 코드의 문제점 : 테스트 메소드를 수행할 때마다 ApplicationContext를 새로 생성 -> 시간이 오래 걸리며, 비효율적.
- 문제 해결
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {
@Autowired
private ApplicationContext context;
private UserDao userDao;
@BeforeEach
void setUp() {
this.userDao = this.context.getBean("userDao", UserDao.class);
}
@Test
void addAndGet() throws SQLException, ClassNotFoundException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
User user = new User();
user.setId("abcd");
user.setName("sb");
user.setPassword("123");
userDao.add(user);
assertThat(userDao.getCount(), is(1));
User user2 = userDao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
}
@Test
public void getUserFailure() throws SQLException, ClassNotFoundException {
userDao.deleteAll();
assertThat(userDao.getCount(), is(0));
userDao.get("unknown"); //예외 발생
}
}
- JUnit 확장 기능 동작 방식
- 테스트가 실행되기 전에 딱 한 번만 ApplicationContext를 만들어두고, 테스트 오브젝트가 만들어 질때마다 특별한 방법을 이용해 ApplicationContext 자신을 테스트 오브젝트의 특정 필드에 주입해주는 것.
테스트 클래스의 컨텍스트 공유
- 여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 ApplicationContext를 사용한다면, 스프링은 서로 다른 테스트 클래스 사이에서도 ApplicationContext를 공유하게 해준다.
테스트에 DI를 이용하는 방법
테스트 코드에 의한 DI
- 테스트 코드 에서 Setter를 이용해서 DI 를 해준다.
- e.g. dataSource가 테스트용 db로 연결되도록 변경.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/applicationContext.xml")
@DirtiesContext //테스트 메소드에서 AC의 구성이나 상태를 변경한다는 것을 test context 프레임워크에 알려준다.
public class UserDaoTest2 {
@Autowired
UserDao dao;
@BeforeEach
void setUp() {
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource);
}
}
- 주의사항 : 빈의 의존관계를 강제로 변경. -> 다른 테스트를 수행할 때 변경된 AC가 계속 사용될 것. -> 바람직하지 않다.
- 해결 :
@DirtiesContext
ApplicationContext를 공유하지 않음. 즉, 테스트 메소드가 수행되고 나면 매번 새로운 AC를 생성.
테스트를 위한 별도의 DI 설정
- 테스트에서 사용될 DataSource 만을 위한 설정파일을 따로 만드는 방법.
- 설정파일을 따로 만들고, 설정파일 경로만 수정해주면 된다.
@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/test-applicationContext.xml")
public class UserDaoTest2 {
//...
}
컨테이너 없는 DI 테스트. ( Q. 생성자를 활용한 방식?)
- UserDaoTest 는 스프링 DI 컨테이너에 의존하지 않는다. 즉, 스프링 API를 직접 사용하거나 AC를 이용하는 코드가 없다.
- 따라서, 테스트 코드에서 직접 오브젝트를 만들고 DI를 해줄 수 있다.
public class UserDaoTest2 {
UserDao dao;
@BeforeEach
void setUp() {
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource);
}
}
- 장점
- AC를 이용하지 않아 코드가 단순하고 이해하기 편하다
- 테스트 실행 시간이 짧다.
학습테스트
- 자신이 사용할 API 나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것
- 장점
- 다양한 조건에 따른 기능을 손쉽게 확인 가능
- 학습테스트 코드를 추후 개발 할 때 참고 가능. 테스트 작성 훈련
@ExtendWith(SpringExtension.class)
@ContextConfiguration
public class JUnitTest {
//ApplicationContext를 하나만 생성해서 공유하는지 테스트
@Autowired
ApplicationContext context;
static Set<JUnitTest> testObjects = new HashSet<>();
static ApplicationContext contextObject = null;
@Test
void test1() {
assertThat(testObjects, not(hasItem(this)));
testObjects.add(this);
assertThat(contextObject == null || contextObject == this.context, is(true));
contextObject = this.context;
}
@Test
void test2() {
assertThat(testObjects, not(hasItem(this)));
testObjects.add(this);
Assertions.assertTrue(contextObject == null || contextObject == this.context);
contextObject = this.context;
}
@Test
void test3() {
assertThat(testObjects, not(hasItem(this)));
testObjects.add(this);
assertThat(contextObject, either(is(nullValue())).or(is(this.context)));
contextObject = this.context;
}
}
버그테스트
- 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트
- 버그가 발견되었을 때 무턱대고 코드를 뒤져가면서 수정하려고 하기보다는 먼저 버그 테스트를 만들어보는 편이 유용하다.
'SPRING' 카테고리의 다른 글
토비의 스프링 4. 예외 (0) | 2022.05.27 |
---|---|
토비의 스프링 3. 템플릿 (0) | 2022.05.27 |
토비의 스프링 1. 오브젝트와 의존관계 (0) | 2022.05.27 |
request 스코프(feat. Provider, Proxy) (0) | 2021.05.21 |
프로토타입 스코프 (0) | 2021.05.20 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- gracefulshutdown
- 토비
- 프록시
- 토비의스프링
- 프로그래머스
- 자바
- BOJ
- 백기선
- 스프링
- 토비의봄TV
- 카카오
- 서비스추상화
- 데코레이터패턴
- java
- 디자인패턴
- 객체지향
- 예외처리
- 자바스터디
- provider
- 템플릿콜백
- 메서드레퍼런스
- 프록시패턴
- 코테
- 코딩테스트
- AOP
- ec2
- c++
- SOLID
- 김영한
- OOP
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
글 보관함