public class UserDaoTest{
public static void main(String[] args) throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext(
"applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new user();
user.setId("user");
user.setName("백기선");
user.setPassword("married");
dao.add(user);
System.out.println(user.getId() + " 등록 성공");
User user2 = dao.get(user.getId());
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
}
}
- 자바에서 쉽게 사용가능한 main() 메소드 이용
- 테스트할 대상인 UserDao의 오브젝트를 가져와 메소드를 호출
- 테스트에 사용할 입력 값(User 오브젝트)을 직접 코드에서 만들어 넣어줌
- 테스트 결과를 콘솔에 입력해줌
- 각 단계의 작업이 에러 없이 끝나면 콘솔에 성공메세지 출력
"테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다"
-토비의 스프링 3.0 中-
작은 단위의 코드에 대해 테스트를 수행한 것
- 단위는 정확한 규격이 정해져 있는 것이 아님.
하나의 관심에 집중해서 테스트를 해볼만한 범위를 단위로 지정
- 개발자 스스로 자신이 작성한 코드가 의도대로 실행되는지를 빠르게 확인받기 위함
- 테스트의 실행시간이 짧아 몇번이고 테스트하기가 편함- 긴 테스트만 수행하게되면 에러의 원인을 찾기가 힘들기 때문
- 각 단위별로 오류를 잡으면서 왔다면 훨씬 수월하게 에러 해결 가능
콘솔에 출력된 값이 일치하는지를 직접 눈으로 확인해야함
main을 매번 실행하는 것은 번거로움
- 전체 테스트시 많은 main메소드를 실행해보아야함
"테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것"
-켄트 벡(xUnit 프레임워크 창시자)-
테스트 결과를 눈으로 비교하여 확인하지 않고 테스트의 성공 실패로 검증하도록 만들기
//user를 저장 후 조회가 제대로 되는지 확인하는 코드
System.out.println(user2.getName());
System.out.println(user2.getPassword());
System.out.println(user2.getId() + " 조회 성공");
// user를 조회한 결과를 직접 눈으로 확인하여 비교해야하는 불편함이 존재
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("조회 테스트 성공");
}
// 테스트가 실패하면 실패한 이유를 출력하도록 하여 눈으로 비교하던 불편함을 해소
JUnit 프레임워크에서 동작하도록 테스트 메소드로 전환하기
import org.junit.Test;
...
public class UserDaoTest{
@Test // JUnit에게 테스트용 메소드임을 알려준다.
public void addAndGet() throws SQLException { //반드시 public으로 선언!
ApplicationContext context = new
ClassPathXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
...
}
}
검증 코드 전환
if/else문을 JUnit이 제공하는 방법으로 전환하기
수정 전 코드
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("조회 테스트 성공");
}
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword, is(user.getPassword()));
import org.junit.Test;
import static org.hamcrest.CoreMatchersis;
import static org.junit.Assert.assertThat;
...
public class UserDaoTest{
@Test
public void addAndGet() throws SQLException {
ApplicationContext context = new
GenericXmlApplicationContext("applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user = new user();
//테스트할 데이터
user.setId("gyumee");
user.setName("박상철");
user.setPassword("springo1");
//
dao.add(user);
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword, is(user.getPassword()));
}
}
////junit 실행 main()메소드////
import org.junit.runner.JUnitCore;
...
public static void main(String[] args){
JUnitCore.main("springbook.user.dao.UserDaoTest");
}
현재 테스트 코드는 DB를 비워주는 작업이 없어 직접 DB를 비우고 테스트해야 함
- 상당히 번거로움
모든 레코드를 삭제해주는 기능
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.preparedStatement("select count(*) from users");
ResultSet rs = ps.executeQuery();
rs.next();
int count = rs.getInt(1);
res.close();
ps.close();
c.close();
return count;
}
...
public class UserDaoTest{
@Test
public void addAndGet() throws SQLException {
...
dao.deleteAll(); //deleteAll()을 통해 DB 비우기
assertThat(dao.getCount(), is(0); //getCount()로 비워졌는지 확인
//테스트할 데이터
user.setId("gyumee");
user.setName("박상철");
user.setPassword("springo1");
//
dao.add(user);
assertThat(dao.getCount(), is(1); // 추가하였을때 count가 오르는지 확인
User user2 = dao.get(user.getId());
assertThat(user2.getName(), is(user.getName()));
assertThat(user2.getPassword, is(user.getPassword()));
}
}
"항상 네거티브 테스트를 먼저 만들라"
-로드 존슨(스프링 창시자)-
public User(String id, String name, String password) {
this.id = id;
this.name = name;
this.password = password;
}
public User{ //자바빈의 규약을 따르는 클래스 생성자를 추가했을 때는 디폴트 생성자를 추가해야 함
}
@Test
public void count() throws SQLException {
ApplicationContext context = new GenericXmlApplicationContext (
"applicationContext.xml");
UserDao dao = context.getBean("userDao", UserDao.class);
User user1 = new User("gyumee", "박상철", "springno1");
User user2 = new User("leegw700", "이길원", "springno2");
User user3 = new User("bumjin", "박범진", "springno3");
dao.deletAll();
assertThat(dao.getCount(), is(0));
dao.add(user1);
assertThat(dao.getCount(), is(1));
dao.add(user2);
assertThat(dao.getCount(), is(2));
dao.add(user3);
assertThat(dao.getCount(), is(3));
}
테스트를 두 개 추가해서 get()메소드의 기능 테스트 보완
...
public class UserDaoTest{
@Test
public void addAndGet() throws SQLException {
...
User user1 = new User("gyumee", "박상철", "springno1");
User user2 = new User("leegw700", "이길원", "springno2");
dao.dleteAll(); //deleteAll()을 통해 DB 비우기
assertThat(dao.getCount(), is(0); //getCount()로 비워졌는지 확인
dao.add(user1);
dao.add(user2);
assertThat(dao.getCount(), is(2); // 추가하였을때 count가 오르는지 확인
User userget1 = dao.get(user1.getId());
assertThat(userget1.getName(), is(user1.getName()));
assertThat(userget1.getPassword, is(user1.getPassword()));
User userget2 = dao.get(user2.getId());
assertThat(userget2.getName(), is(user2.getName()));
assertThat(userget2.getPassword, is(user2.getPassword()));
}
}
get()을 했을 때 조회를 실패하는 예외 테스트 만들기
@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"); // 예외 발생!! - 발생하지않으면 테스트가 실패
}
// 실행시 가져올 값이 없기때문에 SQLException발생 - 테스트 실패
테스트 성공을 위한 코드 수정
수정 전 get()메소드
public User get(String id) throws SQLException{
Connection c = connectionMaker.makeNewConnection();
PrepareStatement ps = c.prepareStatement(
"select * from users where id=?");
ps.setString(1,id);
ResultSet rs = ps.executeQuery();
rs.next();
User user = new User();
user.setId(rs.getString("id"));
user.setName(rs.getString("name"));
user.setPassword(rs.getString("password"));
rs.close();
ps.close();
c.close();
return user;
}
public User get(String id) throws SQLException {
...
ResultSet rs = ps.executeQuery();
User user = null; //초기값 null로 설정
if(rs.next()){ //있다면 값 저장하기 -SQLException이 발생하지 않도록
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); //null이라면 예외 발생
return User;
}
"실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다."
TDD란?
테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 방식
TDD의 장점
- 테스트를 미루지 않고 꼼꼼하게만들 수 있다
- 자연스럽게 단위테스트 형식의 테스트 코드를 만들 수 있다
- 코드를 만들어 테스트를 실행하는 그 사이 간격이 매우 짧음!
JUnit의 애노테이션을 사용해 중복을 제거하자
import org.junit.Before;
...
public class UserDaoTest{
private UserDao dao;
@Before //@Test가 실행되기전 먼저 실행
public void setUp(){ //중복되는 부분 묶기
ApplicationContext context = new GenericXmlApplicationContext (
"applicationContext.xml");
this.dao = context.getBean("userDao", UserDao.class);
}
...
@Test
public void addAndGet() throws SQLException {
...
}
@Test
public void count() throws SQLException {
...
}
@Test(expected=EmptyResultDataAccessException.class)
public void getUserFailure() throws SQLException {
...
}
}
- 테스트 클래스에서 @Test가 붙은 public이고 void형이며 피라미터가 없는 테스트 메소드를 모두 찾는다
- 테스트 클래스의 오브젝트를 하나 만든다
- @Before가 붙은 메소드가 있으면 실행한다
- @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다
- @After가 붙은 메소드가 있으면 실행
- 나머지 테스트 메소드에 대해 2~5번을 반복
- 모든 테스트의 결과를 종합해 보여줌
🔷 픽스처(fixure)
테스트를 수행하는데 필요한 정보나 오브젝트
- UserDaoTest의 dao, add()메소드의 User오브젝트들의 모임
public class UserDaoTest{
private UserDao dao;
private User user1;
private User user2;
private User user3;
@Before //@Test가 실행되기전 먼저 실행
public void setUp(){ //중복되는 부분 묶기
...
this.user1 = new User("gyumee", "박상철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
...
}
ApplicationContext context = new GenericXmlApplicationContext ( "applicationContext.xml");
코드 제거
2. ApplicationContext 타입 변수 생성
3. @Autowired 사용
4. 클래스 레벨에 @RunWith와 @ContextConfiguration 추가
//@RunWith : 애플리케이션 컨택스트를 만들고 관리하는 작업을 진행해줌
//@ContextConfiguration : 애플리케이션 컨텍스트의 설정파일 위치 지정
@RunWith(SpringJUnit4ClassRunner.class)//JUnit확장기능 지정
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
@Autowired //변수타입과 일치하는 컨택스트 내의 빈을 찾아 존재한다면, 값을 주입해줌
private ApplicationContext context; // 테스트 오브젝트가 만들어지면 자동으로 값이 주입
...
@Before
public void setUp() {
// ApplicationContext context = new GenericXmlApplicationContext (
// "applicationContext.xml");
this.dao = context.getBean("userDao", UserDao.class);
this.user1 = new User("gyumee", "박상철", "springno1");
this.user2 = new User("leegw700", "이길원", "springno2");
this.user3 = new User("bumjin", "박범진", "springno3");
}
}
❗ 여러개의 테스트 클래스가 모두 같은 설정파일을 가진 테스트 컨택스트를 사용한다면 스프링은 클래스 사이에 애플리케이션 컨택스트를 공유할 수 있도록 해줌
...
@DirtiesContext // 해당 클래스의 테스트에서는 애플리케이션 컨택스트 상태를 변경함을 알림
public class UserDaoTest {
@Autowired
UserDao dao;
@Before
public void setUp(){
...
//테스트가 사용할 DataSource를 직접 생성
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource); // 수동DI
}
}
UserDao, DataSource구현 클래스 모두 스프링API를 직접 사용하거나 애플리케이션 컨텍스트를 사용하지 않음
- 스프링 DI컨테이너에 의존하고 있지않음
=> 스프링 컨테이너를 이용해 IoC방식으로 생성되고 DI되도록 하는 대신 테스트 코드에서 직접 오브젝트를 만들고 DI해서 사용가능
public class UserDaoTest {
//@Autowired// 제거
UserDao dao;
@Before
public void setUp(){
...
//테스트가 사용할 오브젝트, 관계설정들을 직접 생성
dao = new UserDao();
DataSource dataSource = new SingleConnectionDataSource(
"jdbc:mysql://localhost/testdb", "spring", "book", true);
dao.setDataSource(dataSource); // 수동DI
}
}
- 침투적 기술
- 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술
- 애플리케이션 코드가 해당 기술에 종속되는 결과
- 비침투적 기술
- 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용 가능
- 애플리케이션 코드가 기술에 종속되지 않음
- 스프링이 대표적인 예
자신이 만들지 않은 프레임워크 또는 다른 곳에서 제공한 라이브러리에 대한 테스트를 작성하는 것
자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것
- 다양한 조건에 따른 기능을 손쉽게 확인 가능
- 자동화된 테스트의 장점을 고스란히 가짐- 학습 테스트 코드를 개발 중에 참고 가능
- 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와줌
- 기존에 만든 테스트로 미리 오류를 확인할 수 있음- 테스트 작성에 대한 좋은 훈련이 됨
- 간단한 기능에만 초점을 맞춘 테스트 코드라서 작성이 쉬움- 새로운 기술을 공부하는 과정이 즐거워짐
- 책, 문서 보단 실제로 동작하는 것을 보는 것이 더 재미있을 것
코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트
- 버그에 의해 실패하는 코드를 작성 후, 그 테스트를 성공하도록 코드를 수정
- 테스트의 완성도를 높여줌
- 버그의 내용을 명확하게 분석
- 기술적인 문제를 해결하는데 도움