[Spring] 토비의 스프링 Vol.1 2장 테스트

Shiba·2023년 8월 2일
0

🍀 스프링 정리

목록 보기
3/21
post-thumbnail

📗 테스트

❗ 토비의 스프링 3.1 vol 1 정리입니다.
책을 읽지 않으셨다면 이해가 어려울 수 있습니다!


📖 UserDaoTest 다시 보기

  • UserDaoTest 코드
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 中-

🔷 단위 테스트(Unit Test)

작은 단위의 코드에 대해 테스트를 수행한 것
- 단위는 정확한 규격이 정해져 있는 것이 아님.
하나의 관심에 집중해서 테스트를 해볼만한 범위를 단위로 지정

◼ 단위 테스트가 필요한 이유

  • 개발자 스스로 자신이 작성한 코드가 의도대로 실행되는지를 빠르게 확인받기 위함
    - 테스트의 실행시간이 짧아 몇번이고 테스트하기가 편함
  • 긴 테스트만 수행하게되면 에러의 원인을 찾기가 힘들기 때문
    - 각 단위별로 오류를 잡으면서 왔다면 훨씬 수월하게 에러 해결 가능

📝 UserDaoTest의 문제점

🔷 수동 확인 작업의 번거로움

콘솔에 출력된 값이 일치하는지를 직접 눈으로 확인해야함

🔷 실행 작업의 번거로움

main을 매번 실행하는 것은 번거로움
- 전체 테스트시 많은 main메소드를 실행해보아야함


📖 UserDaoTest 개선하기

"테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것"
-켄트 벡(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("조회 테스트 성공");
}

// 테스트가 실패하면 실패한 이유를 출력하도록 하여 눈으로 비교하던 불편함을 해소

📝 테스트의 효율적인 수행과 결과관리

부제 : main()메소드를 JUnit(프레임워크)으로 전환하기

  • 테스트 메소드로 전환

    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를 비우고 테스트해야 함
- 상당히 번거로움

◼ 일관성있는 테스트 결과를 위한 기능 추가

  • deleteAll()

    모든 레코드를 삭제해주는 기능

public void deleteAll() throws SQLException{
	Connection c= dataSource.getConnection();
    
    PreparedStatement ps = c.prepareStatement("delete from users");
	ps.executeUpdate();
    
    ps.close();
	c.close();
}
  • getCount()

    레코드 갯수를 반환

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()));
    }
}

📝 포괄적인 테스트

"항상 네거티브 테스트를 먼저 만들라"
-로드 존슨(스프링 창시자)-

◼ getCount()메소드 더 꼼꼼하게 테스트하기

  • User 클래스 생성자 만들기
public User(String id, String name, String password) {
	this.id = id;
    this.name = name;
    this.password = password;
}

public User{ //자바빈의 규약을 따르는 클래스 생성자를 추가했을 때는 디폴트 생성자를 추가해야 함
}
  • getCount() 테스트
@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));
}

◼ addAndGet() 테스트 보완하기

테스트를 두 개 추가해서 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;
}
  • 예외를 발생시키도록 수정한 get()메소드
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;
}

📝 테스트를 위한 개발

🔷 테스트 주도 개발(Test Driven Development, TDD)

"실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다."

  • 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 {
        	...
    }
}
  • JUnit 테스트 수행 방식
    테스트 메소드를 실행할 때마다 새로운 오브젝트를 생성.
    1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 피라미터가 없는 테스트 메소드를 모두 찾는다
    2. 테스트 클래스의 오브젝트를 하나 만든다
    3. @Before가 붙은 메소드가 있으면 실행한다
    4. @Test가 붙은 메소드를 하나 호출하고 테스트 결과를 저장해둔다
    5. @After가 붙은 메소드가 있으면 실행
    6. 나머지 테스트 메소드에 대해 2~5번을 반복
    7. 모든 테스트의 결과를 종합해 보여줌

🔷 픽스처(fixure)

테스트를 수행하는데 필요한 정보나 오브젝트
- UserDaoTest의 dao, add()메소드의 User오브젝트들의 모임

  • User픽스처를 적용한 UserDaoTest
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 추가

  • 스프링 테스트 컨텍스트를 적용한 UserDaoTest
//@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");
    }
}

❗ 여러개의 테스트 클래스가 모두 같은 설정파일을 가진 테스트 컨택스트를 사용한다면 스프링은 클래스 사이에 애플리케이션 컨택스트를 공유할 수 있도록 해줌

📝 테스트에 DI 적용하기

◼ 테스트 코드에 의한 DI

...
@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
    }
}

◼ 컨테이너 없는 DI 테스트

UserDao, DataSource구현 클래스 모두 스프링API를 직접 사용하거나 애플리케이션 컨텍스트를 사용하지 않음
- 스프링 DI컨테이너에 의존하고 있지않음
=> 스프링 컨테이너를 이용해 IoC방식으로 생성되고 DI되도록 하는 대신 테스트 코드에서 직접 오브젝트를 만들고 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나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것

◼ 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인 가능
    - 자동화된 테스트의 장점을 고스란히 가짐
  • 학습 테스트 코드를 개발 중에 참고 가능
  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와줌
    - 기존에 만든 테스트로 미리 오류를 확인할 수 있음
  • 테스트 작성에 대한 좋은 훈련이 됨
    - 간단한 기능에만 초점을 맞춘 테스트 코드라서 작성이 쉬움
  • 새로운 기술을 공부하는 과정이 즐거워짐
    - 책, 문서 보단 실제로 동작하는 것을 보는 것이 더 재미있을 것

📝 버그 테스트

코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트
- 버그에 의해 실패하는 코드를 작성 후, 그 테스트를 성공하도록 코드를 수정

◼ 장점

  • 테스트의 완성도를 높여줌
  • 버그의 내용을 명확하게 분석
  • 기술적인 문제를 해결하는데 도움

❗ 더욱 상세한 내용을 알고싶으시다면 책을 구매하시는 것을 추천드립니다.

profile
모르는 것 정리하기

0개의 댓글