이번에 토비님이 진행하시는 토비의 스프링 읽기모임에 들어갔다.
책을 읽으며 최대한 머릿속에 담기 위해 정리해봤는데, 해당 내용을 블로그에 기록하면 두고두고 볼 수 있을 것 같아 포스팅하게 되었다.
사진 자료는 저작권 문제로 제외했다.
2장은 개인적으로는 쉬어가는 장이었다...!👀 이미 우테코 미션 등에서 Junit5
를 통해 TDD
를 실천했었고 그 유용성을 실감했기 때문이다.
속닥속닥에서, 실제 배포해 사용자가 있을 프로젝트를 만들어나가면서, 기능 구현에 바빠 이전에 목표로 했던(90% 이상의) 테스트 커버리지를 갖지 못한 점이 더 아쉬웠다. 앞으로 계속 테스트 코드를 추가할 의지가 생겼다.
책을 읽으며 버그테스트의 유용성에 대해서도 실감할 수 있었다.
"충분한 검증이 없는 테스트는 없는 것보다 나쁘다. 네거티브 테스트 먼저 작성하는 습관을 들이자."
이 단락에 많이 공감했다. 미니 프로젝트(aka.미션)를 진행하면서 낙관적인 테스트가 짜여진 클래스에 대해서는 믿고 간 적이 많다. 그러다보니 버그가 터졌을 때 더 당황하고 대응하기 까다로웠던 적이 많다.
앞으로 취직하고 회사 안에서 테스트로 시작하는 개발까진 아니어도, 테스트가 주도하는 개발을 하고 싶을 때, 이번 장에서 정리한 내용들을 통해 팀원들과 토론할 수 있을 것 같다.
💬
추가적인 얘기로, 이번 읽기모임에서 운좋게 테스트 관련 많은 질문들을 드릴 수 있었다. MockBean
등의 목 라이브러리 사용방식과 인터페이스를 통한 테스트더블 구현 중 어느 방향이 좋을지 항상 고민해 질문드렸다. 결론은 정답은 없되, Mockito
라이브러리의 이점을 활용하는 것은 좋은 방향이라는 것이었다.
관련해 토프링 읽기모임에서 나왔던 얘기들을 노션 링크로 공유한다. 테스트 범위부터, 테스트를 위한 다양한 방법론에 대한 고찰을 할 수 있어 좋았다!
스프링이 개발자에게 제공하는 가장 중요한 가치가 무엇이냐고 질문한다면 나는 주저하지 않고 객체지향과
테스트
라고 대답할 것이다.
스프링으로 개발을 하면서 테스트를 만들지 않는다면 이는 스프링이 지닌 가치의 절반을 포기하는 셈이다.
개발자들이 낭만이라고도 생각하는 눈물 젖은 커피와 함께 며칠간 밤샘을 하며 오류를 잡으려고 애쓰다가 전혀 생각지도 못했던 곳에서 간신히 찾아낸 작은 버그 하나의 추억이라는 건, 사실 진작에 충분한 테스트를 했었다면 쉽게 찾아냈을 것을 미루고 미루다 결국 커다란 삽질로 만들어버린 어리석은 기억일 뿐이다.
일반적으로 테스트하기 좋은 코드가 좋은 코드일 가능성이 높다. 그 반대도 마찬가지다. 나는 이제까지 테스트하기 불편하게 설계된 좋은 코드를 본 기억이 없다.
만든 코드는 어떤 방법으로든 테스트
해야 한다!
💬 웹을 통한 DAO 테스트 방법의 문제점
웹 화면을 통해 값을 입력하고, 기능을 수행하고, 결과를 확인하는 방법은 가장 흔히 쓰이는 방법이지만, DAO에 대한 테스트로서는 단점이 너무 많다.
💬 단위 테스트
테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.
관심사의 분리라는 원리가 여기에도 적용된다. 테스트의 관심이 다르다면 테스트할 대상을 분리하고 집중해서 접근해야 한다.
이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(unit test
)라고 한다.
💬 DB가 사용되어도 단위테스트인가?
어떤 개발자는 테스트 중에 DB가 사용되면 단위 테스트가 아니라고도 한다. 그럼 UserDaoTest
는 단위 테스트가 아니라고 봐야 할까?
NO!
UserDaoTest
를 수행할 때 매번 테이블의 내용을 비웠다.다만, 통제할 수 없는 외부의 리소스에 의존하는 테스트는 단위 테스트가 아니라고 보기도 한다.
UserDaoTest
가 단위 테스트로서 가치가 없어진다.💬 통합테스트도 필요하다
길고 많은 단위가 참여하는 테스트(통합테스트)도 필요하다.
💬 자동수행 테스트 코드
테스트가 자동 수행되지 않는다면?
따라서 테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요하다.
💬 테스트용 클래스를 분리하라
애플리케이션을 구성하는 클래스 안에 테스트 코드를 포함시키는 것보다는 별도로 테스트용 클래스를 만들어서 테스트 코드를 넣는 편이 낫다.
UserDao
클래스 하나만 존재했으니, 그 안에 main()
메소드를 만들어 사용했다.UserDaoTest
라는 테스트용 클래스를 만들자.💬 지속적인 개선과 점진적인 개발을 위한 테스트
작은 단계로 테스트를 수행하자.
public class XmlUserDaoTest {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User user = new User();
user.setId("12341234");
user.setName("제이크22522");
user.setPassword("jakejake");
userDao.add(user);
System.out.println(user.getId() + " register succeeded");
User user2 = userDao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " query succeeded");
}
}
💬 수동 확인 작업의 번거로움
UserDaoTest
는 테스트를 수행하는 과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다.
add()
에서 User 정보를 DB에 등록하고, 이를 다시 get()
을 이용해 가져왔을 때 입력한 값과 가져온 값이 일치하는지를 테스트 코드는 확인해주지 않는다.💬 실행 작업의 번거로움
아무리 간단히 실행 가능한 main()
메소드라고 하더라도 매번 그것을 실행하는 것은 제법 번거롭다.
만약 DAO가 수백 개가 되고 그에 대한 main()
메소드도 그만큼 만들어진다면?
main()
메소드를 수백 번 실행해야 한다.자동화된 테스트를 위한
xUnit
프레임워크를 만든 켄트 벡은 테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것이라고 했다.
모든 테스트는 성공과 실패의 두 가지 결과를 가질 수 있다.
테스트 에러
: 테스트가 진행되는 동안에 에러가 발생해서 실패테스트 실패
: 테스트 작업 중에 에러가 발생하진 않았지만 그 결과가 기대한 것과 다르게 나옴테스트 프레임워크를 통해 두 경우 모두 검증할 수 있다.
JUnit
은 프로그래머를 위한 자바 테스팅 프레임워크이다.
public class UserDaoTest {
@Test
public void addAndGet() throws SQLException {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User userToAdd = new User();
userToAdd.setId("hunch");
userToAdd.setName("헌치");
userToAdd.setPassword("password");
userDao.add(userToAdd);
User userToGet = userDao.get("hunch");
Assertions.assertEquals(userToAdd.getId(), userToGet.getId());
Assertions.assertEquals(userToAdd.getName(), userToGet.getName());
Assertions.assertEquals(userToAdd.getPassword(), userToGet.getPassword());
}
}
💬 JUnit 테스트로 전환
JUnit
은 프레임워크다!
따라서 프레임워크에서 동작하는 코드는
main()
메소드도 필요 없고💬 테스트 메소드 전환
기존에 만들었던 main()
메소드 테스트는 제어권을 직접 가졌다. 프레임워크에 적용하기엔 적합하지 않다. 테스트 코드를 main()
에서 일반 메소드로 옮기자.
JUnit
테스트 메소드 요구조건
public
으로 선언돼야 한다.@Test
애노테이션을 붙여줘야 한다.💬 검증 코드 전환
JUnit
은 예외가 발생하거나 assertThat()
에서 실패하지 않고 테스트 메소드의 실행이 완료되면 테스트가 성공했다고 인식한다.JUnit
은 테스트 성공/실패를 다양한 방법으로 알려준다.💬 JUnit 테스트 실행
JUnit
프레임워크를 이용해 앞에서 만든 테스트 메소드를 실행하도록 코드를 만들어보자.
JUnit
프레임워크도 자바 프로그램이므로 초기에 JUnit
프레임워크를 시작시켜 줘야 한다.테스트 에러
: JUnit
은 assertThat()
을 이용해 검증을 했을 때 기대한 결과가 아니면 이 AssertionError
를 던진다. 따라서 assertThat()
의 조건을 만족하지 못하면 테스트는 더 이상 진행되지 않고 JUnit
은 테스트가 실패했음을 알게 된다.테스트 예외
: 테스트 수행 중에 일반 예외가 발생한 경우에도 마찬가지로 테스트 수행은 중단되고 테스트는 실패한다.가장 좋은 JUnit
테스트 실행 방법은 자바 IDE
에 내장된 JUnit 테스트 지원 도구를 사용하는 것이다.
💬 IDE
IDE
를 통해 JUnit
테스트의 실행과 그 결과를 확인하는 방법
매우 간단하고 직관적이며 소스와 긴밀하게 연동돼서 결과를 볼 수 있다.
💬 빌드 툴
여러 개발자가 만든 코드를 모두 통합해서 테스트를 수행해야 할 때도 있다.
JUnit
테스트를 실행하고 그 결과를 메일 등으로 통보받는 방법을 사용하면 된다.테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 하면 안된다. 일관성있는 결과를 위해 DB 초기화가 필요하다.
💬 deleteAll()의 getCount() 추가
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;
}
일관성 있는 결과를 보장하기 위해 UserDao에 두 기능을 추가하자.
deleteAll()
: USER 테이블의 모든 레코드를 삭제한다.getCount()
: USER 테이블의 레코드 개수를 돌려준다.💬 deleteAll()과 getCount()의 테스트
@Test
public void addAndGet() throws SQLException {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
// `deleteAll()`, `getCount()` 기능 동작 확인
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
User userToAdd = new User();
userToAdd.setId("jinkyu1");
userToAdd.setName("진규");
userToAdd.setPassword("password");
userDao.add(userToAdd);
// 유저가 있을 때, `getCount()` 기능 동작 확인
assertEquals(userDao.getCount(), 1);
User userToGet = userDao.get("jinkyu1");
// 유저가 제대로 등록되었는지 확인
assertEquals(userToAdd.getId(), userToGet.getId());
assertEquals(userToAdd.getName(), userToGet.getName());
assertEquals(userToAdd.getPassword(), userToGet.getPassword());
// 유저가 있을 때, `deleteAll()`, `getCount()` 기능 동작 확인
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
}
deleteAll()
이 기대한 대로 동작한다면, getCount()
로 레코드의 개수를 가져올 경우 0이 나와야 한다.
getCount()
테스트
add()
를 수행하고 나면 레코드 개수가 0에서 1로 바뀌어야 한다.add()
메소드를 실행한 뒤에 getCount()
의 결과를 한 번 더 확인해보자.deleteAll()
직후에는 0이 나오고 add()
직후에는 1이 나온다면, getCount()
의 기능이 검증된다.deleteAll()
테스트
getCount()
가 바르게 동작해야 한다.deleteAll()
직후에 getCount()
가 0이 나오면 deleteAll()
의 기능이 검증된다.💬 동일한 결과를 보장하는 테스트
이제 테스트 메소드별로 DB가 초기화된다.
설령 테스트 수행 직전에 DB에 작업을 하느라 USER 테이블에 정보를 넣어뒀더라도 일관된 결과를 얻을 수 있다.
테스트를 안 만드는 것도 위험한 일이지만, 성의 없이 테스트를 만드는 바람에 문제가 있는 코드인데도 테스트가 성공하게 만드는 건 더 위험하다.
getCount()
는 정말 검증되었는가?
💬 getCount() 테스트
@Test
@DisplayName("회원 카운팅")
public void getCount() throws SQLException {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
User user1 = new User("user1", "김똘일", "1234");
User user2 = new User("user2", "김똘이", "1234");
User user3 = new User("user3", "김똘삼", "1234");
User user4 = new User("user4", "김똘사", "1234");
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
userDao.add(user1);
assertEquals(userDao.getCount(), 1);
userDao.add(user2);
assertEquals(userDao.getCount(), 2);
userDao.add(user3);
assertEquals(userDao.getCount(), 3);
userDao.add(user4);
assertEquals(userDao.getCount(), 4);
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
}
JUnit은 하나의 클래스 안에 여러 개의 테스트 메소드가 들어가는 것을 허용한다.
@Test
가 붙어 있고public
접근자가 있으며void
형이고테스트는 순서에 영향받아선 안된다!
💬 addAndGet() 테스트 보완
@Test
@DisplayName("회원 추가 및 불러오기")
public void addAndGet() throws SQLException {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
// `deleteAll()`, `getCount()` 기능 동작 확인
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
User user1 = new User();
user1.setId("jinkyu1");
user1.setName("진규");
user1.setPassword("password");
userDao.add(user1);
// 유저가 있을 때, `getCount()` 기능 동작 확인
assertEquals(userDao.getCount(), 1);
User user2 = new User();
user2.setId("jake2");
user2.setName("제이크");
user2.setPassword("password");
userDao.add(user2);
// 유저가 있을 때, `getCount()` 기능 동작 확인 2
assertEquals(userDao.getCount(), 2);
User user1Get = userDao.get("jinkyu1");
// 유저가 제대로 불러와지는지 확인
assertEquals(user1.getId(), user1Get.getId());
assertEquals(user1.getName(), user1Get.getName());
assertEquals(user1.getPassword(), user1Get.getPassword());
User user2Get = userDao.get("jake2");
// 항상 같은 유저를 불러오는 것은 아닌지, 유저가 제대로 불러와지는지 확인
assertEquals(user2.getId(), user2Get.getId());
assertEquals(user2.getName(), user2Get.getName());
assertEquals(user2.getPassword(), user2Get.getPassword());
// 유저가 있을 때, `deleteAll()`, `getCount()` 기능 동작 확인
userDao.deleteAll();
assertEquals(userDao.getCount(), 0);
}
id를 조건으로 해서 사용자를 검색하는 기능을 가진 get()에 대한 테스트는 조금 부족한 감이 있다.
User를 하나 더 추가해서 두 개의 User를 add() 하고, 각 User의 id를 파라미터로 전달해서 get()을 실행하도록 만들어보자.
💬 get() 예외조건에 대한 테스트
@Test
@DisplayName("존재하지 않는 회원을 조회할 때")
public void getUserFailure() {
ApplicationContext applicationContext = new GenericXmlApplicationContext("spring/applicationContext.xml");
UserDao userDao = applicationContext.getBean(UserDao.class);
// 스프링이 제공하는 EmptyResultDataAccessException 예외가 나타나게 만들자.
assertThrows(EmptyResultDataAccessException.class, () -> {
userDao.get("not_existing_user_id");
});
}
get() 메소드에 전달된 id 값에 해당하는 사용자 정보가 없을 때
각기 장단점이 있다. 여기서는 후자의 방법을 써보자.
테스트 진행 중에 특정 예외가 던져지면 테스트가 성공한 것이고, 예외가 던져지지 않고 정상적으로 작업을 마치면 테스트가 실패했다고 판단해야 한다.
이런 경우를 위해 JUnit은 특별한 방법을 제공해준다.
@Test에 expected를 추가해놓으면
예외가 반드시 발생해야 하는 경우를 테스트하고 싶을 때 유용하게 쓸수 있다.
💬 테스트를 성공시키기 위한 코드의 수정
이제부터 할 일은 이 테스트가 성공하도록 get() 메소드 코드를 수정하는 것이다.
public User get(String id) throws SQLException {
// 1.2.2 중복 코드의 메소드 추출
Connection c = dataSource.getConnection();
PreparedStatement ps = c.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();
c.close();
if(user == null) throw new EmptyResultDataAccessException(1);
return user;
}
코드를 수정하고 나서 테스트를 실행해봐서 테스트가 성공한다면 원하는 기능을 가진 코드가 제대로 만들어졌다고 보면 된다. 테스트가 실패하면 원인을 파악해서 코드를 다시 수정하고 성공할 때까지 테스트를 돌려보자.
💬 포괄적인 테스트
스프링의 창시자인 로드 존슨은 항상 네거티브 테스트를 먼저 만들라는 조언을 했다.
테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이자!
get()
메소드의 경우
많은 전문적인 개발자가 테스트를 먼저 만들어 테스트가 실패하는 것을 보고 나서 코딩하는 방법을 개발 방법을 적극적으로 사용하고 있다.
💬 기능설계를 위한 테스트
테스트 코드는 마치 잘 작성된 하나의 기능정의서
처럼 보인다.
보통 기능설계, 구현, 테스트라는 일반적인 개발 흐름의 기능설계에 해당하는 부분을 테스트 코드가 일부분 담당하고 있다.
💬 테스트 주도 개발(TDD
, Test Driven Development)
과거 개발자들은 엔터프라이즈 애플리케이션의 테스트를 만들기가 매우 어렵다고 생각해 테스트 코드를 짜지 않았다.
하지만 스프링은 테스트하기 편리한 구조의 애플리케이션을 만들게 도와줄 뿐만 아니라, 엔터프라이즈 애플리케이션 테스트를 빠르고 쉽게 작성할 수 있는 매우 편리한 기능을 많이 제공하므로 테스트를 하자!
- 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발 방식.
- 만들고자 하는 기능의 내용을 담고 있으면서(기능 설계) 만들어진 코드를 검증도 해줄 수 있다.
장점
TDD
에서는 테스트 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 한 짧게 가져가도록 권장한다.
테스트 결과가 일정하게 유지된다면 얼마든지 리팩토링을 해도 좋다.
💬 @Before
JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같다.
장점
주의사항
왜 테스트 메소드를 실행할 때마다 새로운 오브젝트를 만드는 것일까? 그냥 테스트 클래스마다 하나의 오브젝트만 만들어놓고 사용하는 편이 성능도 낫고 더 효율적이지 않을까?
💬 픽스처
테스트를 수행하는 데 필요한 정보나 오브젝트를 fixture
라고 한다.
픽스처는 여러 테스트에서 반복적으로 사용되기 때문에 @Before
메소드를 이용해 생성해두면 편리하다.
이제 테스트 코드도 어느 정도 깔끔하게 정리를 마쳤다. 하지만 아직 한 가지 찜찜한 부분이 남아 있는데, 바로 애플리케이션 컨텍스트 생성 방식이다.
시간
에러
테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이다. 하지만 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.
문제는 JUnit이 매번 테스트 클래스의 오브젝트를 새로 만든다는 점이다. 따라서 여러 테스트가 함께 참조할 애플리케이션 컨텍스트를 오브젝트 레벨에 저장해두면 곤란하다.
→ 스태틱 필드에 애플리케이션 컨텍스트를 저장해두면 어떨까?
하지만 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 더 편리하다.
💬 스프링 테스트 컨텍스트 프레임워크 적용
테스트에서 사용할 ApplicationContext
하나를 만들고, 공유하게 된다.
// @SpringBootTest (SpringBoot)
// @Runwith(SpringJUnit4ClassRunner.class) (JUnit4)
@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class UserDaoTest {
@Autowired ApplicationContext applicationContext;
UserDao userDao;
@BeforeEach
public void setUp() {
System.out.println("applicationContext = " + applicationContext);
System.out.println("this = " + this);
this.userDao = this.applicationContext.getBean("userDao", UserDao.class);
@ExtendWith
는 JUnit5에서 테스트 클래스를 확장할 때 쓰이는 애노테이션이다.@ContextConfiguration
은 locations에서 ApplicationContext
에 사용될 xml
파일의 위치를 지정해줄 수 있다.@Autowired
는 테스트용 ApplicationContext
내부에 있는 정의된 타입의 빈(위 경우 ApplicationContext
)을 찾아서 자동으로 주입한다.💬 테스트 메소드의 컨텍스트 공유
그렇다면 context 변수에 어떻게 애플리케이션 컨텍스트가 들어 있는 것일까?
💬 테스트 클래스의 컨텍스트 공유
두 개의 테스트 클래스가 같은 설정파일을 사용하는 경우?
💬 @Autowired
굳이 컨텍스트를 가져와 getBean()을 사용하는 것이 아니라, 아예 UserDao 빈을 직접 DI 받을 수도 있다.
인터페이스인 DataSource 타입으로 변수를 선언해도 된다.
SimpleDriverDataSource
를 생성하고 사용하면 안 될까?
NO! 인터페이스를 두고 DI를 적용해야 한다.
💬 테스트 코드에 의한 DI
테스트할 때 운영용 DataSource를 이용하면 안된다!
DI를 이용해서 테스트 중에 DAO가 사용할 DataSource 오브젝트를 바꿔주자!
장점
주의사항
💬 @DirtiesContext
💬 테스트를 위한 별도의 DI 설정
아예 테스트에서 사용될 DataSource 클래스가 빈으로 정의된 테스트 전용 설정파일을 따로 만들어두는 방법을 이용해도 된다.
💬 컨테이너 없는 DI 테스트
DI는 객체지향 프로그래밍 스타일이다. 따라서 DI를 위해 컨테이너가 반드시 필요한 것은 아니다. DI 컨테이너나 프레임워크는 DI를 편하게 적용하도록 도움을 줄 뿐, 컨테이너가 DI를 가능하게 해주는 것은 아니다.
아예 스프링 컨테이너를 사용하지 않고 테스트를 만들 수도 있다.
public class UserDaoTest {
UserDao dao;
//...
@BeforeEach
public void setUp() {
//...
dao = new UserDao();
DataSource = new SingleConnectionDataSource(
"jdbc:postgresql://localhost/test", "postgres", "password", true
);
dao.setDataSource(dataSource);
}
}
장점
단점
💬 비침투적 기술
비침투적(noninvasive)인 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다.
💬 DI를 이용한 테스트 방법 선택
그렇다면 DI를 테스트에 이용하는 세 가지 방법 중 어떤 것을 선택해야 할까? 세 가지 방법 모두 장단점이 있고 상황에 따라 유용하게 쓸 수 있다.
@DirtiesContext
애노테이션을 붙이는 것을 잊지 말자.나는 새로운 프레임워크를 사용하게 되거나 새로운 기술을 공부할 때는 항상 테스트 코드를 먼저 만들어본다. 테스트 코드를 만드는 과정을 통해 API의 사용 방법도 익히고 내가 가진 기술에 대한 지식도 검증할 수 있다.
학습 테스트를 통해 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익힐 수 있다.
💬 JUnit 테스트 오브젝트 테스트
public class JUnitTest {
static JUnitTest testObject;
@BeforeAll
public static void beforeAll() {
testObject = new JUnitTest();
}
@AfterEach
public void afterEach() {
testObject = this;
}
@Test
public void test1() {
assertNotSame(testObject, this);
System.out.println("testObject = " + testObject);
System.out.println("this = " + this);
}
@Test
public void test2() {
assertNotSame(testObject, this);
System.out.println("testObject = " + testObject);
System.out.println("this = " + this);
}
@Test
public void test3() {
assertNotSame(testObject, this);
System.out.println("testObject = " + testObject);
System.out.println("this = " + this);
}
}
💬 스프링 애플리케이션 컨텍스트 테스트
@ExtendWith(SpringExtension.class) // (JUnit5)
@ContextConfiguration(locations="/spring/applicationContext.xml")
public class ApplicationContextTest {
@Autowired ApplicationContext applicationContext;
static Set<ApplicationContext> applicationContexts = new HashSet<>();
@AfterAll
public static void afterAll() {
assertEquals(applicationContexts.size(), 1);
}
@Test
public void test1() {
applicationContexts.add(applicationContext);
}
@Test
public void test2() {
applicationContexts.add(applicationContext);
}
@Test
public void test3() {
applicationContexts.add(applicationContext);
}
}
버그 테스트(bug test
)란?
장점
💬 경계값 분석(boundary value analysis)
에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다.
보통 숫자의 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트해보면 도움이 될 때가 많다.
main()
을 이용하지 말고, JUnit
프레임워크를 이용하면 테스트 자동화가 가능하다.@BeforeEach
, @AfterEach
를 사용해서 테스트 메소드들의 공통 준비 작업과 정리 작업을 처리할 수 있다.@Autowired
를 사용하면 컨텍스트의 빈을 테스트 오브젝트에 DI할 수 있다.