[Spring]토비의 스프링3.1 - 2장

sally·2021년 7월 28일
0
post-thumbnail

2장에서는 테스트란 무엇이며, 가치와 장점, 활용 전략, 스프링과의 관계를 살펴본다. 또한 대표적인 테스트 프레임워크를 이용한 학습 전략을 알아 볼 것이다.


테스트👀

스프링이 개발자에게 제공하는 중요한 가치 두가지는 객체지향테스트이다. 애플리케이션은 계속 변하고 복잡해여 간다. 변화에 대응하는 첫 번째 전략이 확장과 변화를 고려한 객체지향적 설계라면 두 번째 전략은 코드를 확신할 수 있게 해주고 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이다.

Junit 테스트

편리하게 테스트를 수행하고 결과를 확인하는 프레임워크를 스프링에서 제공해준다. 바로 Junit 테스트 프레임워크다.

Junit 프레임워크는 두가지 조건을 따라야 한다.

  • public으로 선언되어야 할 것
  • 메소드에 @Test annotaion 필요
@SpringBootTest
class UserDaoTest {

    //JUnit에게 테스트용 메소드임을 알려줌
    @Test
    public void addAndGet() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory.class);
        UserDao_Relation userDao = context.getBean("userDao",UserDao_Relation.class);

        User user = new User();
        user.setId("sykwon");
        user.setName("권수연");
        user.setPassword("suyeon");

        userDao.add(user);

        System.out.println(user.getId() + " 등록 성공");

        User user2 = userDao.get(user.getId());
        
        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을 사용한 코드로 바꿔보도록 하겠다! ✌

검증 코드 전환

테스트 결과를 검증하는 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("조회 테스트 성공");
        }

전환 후

assertEquals(user.getName(),user2.getName());
assertEquals(user.getPassword(),user2.getPassword());

assertThat() 은 첫 번째 파라미터 값을 뒤에 나오는 matcher 조건으로 비교해서 일치하면 다음으로 넘어가고 아니면 테스트 실패하도록 만들어 준다.

또한 JUnit은 테스트 메소드의 실행이 완료되면 테스트가 성공했다고 인식하기 때문에 "테스트 성공"과 같은 메시지를 따로 출력할 필요도 없다.

테스트 결과의 일관성

현재 UserDaoTest의 문제는 이전 테스트 때문에 DB에 등록된 중복 데이터가 있을 수 있다. 문제를 해결하기 위해서 테스트를 마치고 나면 테스트가 등록한 사용자 정보를 삭제해서 테스트를 수행하기 이전 상태로 만들어주는 것이다.

일관성 있는 결과를 보장하기 위해서 deleteAll()getCount() 메소드를 추가해줄것이다.

deleteAll()

user 테이블의 모든 레코드를 삭제하는 기능을 가진 메소드를 UserDao에 추가한다.

    public void deleteAll() throws SQLException{
        Connection c = dataSource.getConnection();

        PreparedStatement ps = c.prepareStatement("delete from users");
        
        ps.executeUpdate();
        
        ps.close();
        c.close();
    }

getCount()

user테이블의 레코드 개수를 돌려준다.

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

새로 만든 메소드들을 이용하여 테스트 코드를 개선해보자. 여기서 주의해야 할 점은 JUnit이 테스트의 실행 순서를 보장해주지 않는다는 것이다. 테스트 결과가 테스트 실행 순서에 영향을 받는다면 테스트를 잘못 만든 것이다.

    @Test
    void addAndGet() throws SQLException, ClassNotFoundException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory_dataSource.class);
        UserDao_dataSource userDao = context.getBean("userDao",UserDao_dataSource.class);

        User user1 = new User("sykwon","권수연","1234");
        User user2 = new User("chjang","장창훈","1234");

        userDao.deleteAll();
        assertEquals(userDao.getCount(),0);

        userDao.add(user1);
        userDao.add(user2);
        assertEquals(userDao.getCount(),2);


        User userget1 = userDao.get(user1.getId());
        assertEquals(userget1.getName(),user1.getName());
        assertEquals(userget1.getPassword(),user1.getPassword());

        User userget2 = userDao.get(user2.getId());
        assertEquals(userget2.getName(),user2.getName());
        assertEquals(userget2.getPassword(),user2.getPassword());
    }

테스트 시나리오

  • user 테이블의 데이터를 모두 지운다.
  • getCount()가 0임을 확인한다.
  • user를 insert한다.
  • getCount()가 증가함을 확인한다.
  • id에 해당하는 정확한 user 정보를 가져오는지 확인한다.

get() 예외조건 테스트

get() 메소드에 전달된 id값에 해당하는 사용자 정보가 없다면 어떻게 처리하면 좋을까?

  • null과 같은 특별 값 리턴
  • id에 해당하는 정보를 찾을 수 없다고 예외 던지기

후자의 방법을 사용하여 보자

JUnit은 예외조건 테스트를 위한 특별한 방법을 제공해준다.

    @Test()
    public void getUserFailure() throws SQLException {
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory_dataSource.class);
        UserDao_dataSource userDao = context.getBean("userDao",UserDao_dataSource.class);

        userDao.deleteAll();
        assertEquals(userDao.getCount(),0);
		//테스트 중에 발생할 것으로 기대하는 예외 클래스 지정
        assertThrows(EmptyResultDataAccessException.class, () -> {
            userDao.get("unkown_id");
        });
    }

assertThrows에 필요한 클래스를 등록하고 람다식으로 예외를 던질 실행문을 작성하면 된다.

포괄적인 테스트

개발자가 테스트를 만들때 자주하는 실수가 있다. 바로 성공하는 테스트만 골라서 만드는 것! 그래서 테스트를 작성할 때 부정적인 케이스를 먼저 만드는 습관을 들이는 것이 좋다. 예외적인 상황을 빠뜨리지 않는 꼼꼼한 개발이 가능하다.

TDD(테스트 주도 개발)

테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 작성하는 방식의 개발 방법을 테스트 주도 개발 TDD라고 한다. "실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다"는 것이 TDD의 기본 원칙이다.

테스트 코드 개선

UserDaoTest 코드에서 아래 코드가 기계적으로 반복된다.

ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory_dataSource.class);
UserDao_dataSource userDao = context.getBean("userDao",UserDao_dataSource.class);

메소드 추출 리팩토링 방법 말고 JUnit이 제공하는 기능을 활용해서 중보 코드를 제거해보자.

JUnit이 테스트를 수행하는 방식은 다음과 같다

  1. 테스트 클래스에서 @Test가 붙은 public이고 void형이며 파라미터가 없는 테스트 메소드를 모두 찾는다.
  2. 테스트 클래스의 오브젝트를 하나 만든다
  3. @BeforeEach가 붙은 메소드가 있으면 실행한다.
  4. @Test가 붙은 메소드를 하나 호출하고 결과를 저장해둔다.
  5. @After가 붙은 메소드가 있으면 실행한다.
  6. 나머지 테스트 메소드에 대해 2~5번을 반복한다.
  7. 모든 테스트의 결과를 종합해서 돌려준다.

@BeforeEach annotation에 위의 코드를 넣고 contextdao를 인스턴스 변수로 분리하면 모든 테스트에서 사용할 수 있다.

class UserDaoTest_step5 {
    private UserDao_dataSource userDao;

    @BeforeEach
    public void setup(){
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory_dataSource.class);
        this.userDao = context.getBean("userDao",UserDao_dataSource.class);
    }
    
    .
    .
    .
}

꼭 기억해야 할 사항은 각 테스트 메소드를 실행할 때 마다 테스트 클래스의 오브젝트를 새로 만든다는 점이다. 한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다. 테스트 클래스가 @Test 메소드를 두개 갖고 있다면, 테스트가 실행되는 중에 JUnit은 클래스의 오브젝트를 두번 만든다.

픽스쳐

테스트를 수행하는데 필요한 정보나 오브젝트를 픽스쳐(fixture)라고 한다.

픽스쳐는 여러 테스트에서 반본적으로 사용되기 때문에 @BeforeEach 메소드를 이용해 생성해두면 편리하다.

class UserDaoTest_step6 {
    private UserDao_dataSource userDao;
    private User user1;
    private User user2;

    @BeforeEach
    public void setup(){
        ApplicationContext context = new AnnotationConfigApplicationContext(DaoFactory_dataSource.class);
        this.userDao = context.getBean("userDao",UserDao_dataSource.class);
        this.user1 = new User("sykwon","권수연","1234");
        this.user2 = new User("chjang","장창훈","1234");
    }
}

스프링 테스트 적용

현재 테스트 코드는 @BeforeEach메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션 컨텍스드도 세번 만들어진다. 빈이 많아지고 복잡해지면 애플리케이션 컨텍스트 생성에 적지 않은 시간이 걸릴 수 있다.

애플리케이션 컨텍스트 같은 경우 스태틱 필드에 저장해두고 공통 변수로 사용하면 효율적으로 사용할 수 있다. JUnit은 테스트 클래스 전체에 거쳐서 딱 한번만 실행해되는 @BeforeClass 스태틱 메소드를 지원한다. 하지만 스프링이 직접 제공해주는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하면 보다 편리하다.

@SpringBootTest(classes = TobySpringApplicationTests.class)
@ContextConfiguration(classes={DaoFactory_dataSource.class})
class UserDaoTest_step7 {
    @Autowired
    private ApplicationContext context;
	.
    .
    .

    @BeforeEach
    public void setup(){
        this.userDao = context.getBean("userDao",UserDao_dataSource.class);

@ContextConfiguration은 자동으로 만들어 줄 애플리케이션 컨텍스트 설정 파일 위치를 지정해주는 어노테이션이다.

또한 테스트를 위해 설정 클래스파일을 하나 더 만들어두고 @ContextConfiguration을 사용하면 쉽게 개발용 테스트 클래스를 실해할 수 있다. 이렇게 하면 테스트 환경에서 적합한 구성을 가진 설정파일을 이용해서 테스트를 진행할 수 있다!

정리

  • 테스트는 자동화해야 하고, 빠르게 실행할 수 있어야 한다.
  • 테스트 결과는 일관성이 있어야 한다. 코드의 변경 없이 환경이나 테스트 실행 순서에 따라서 결과가 달라지면 안된다.
  • 코드 작성과 테스트 수행의 간격이 짧을수록 효과적이다.
  • @Before, @After를 사용해서 테스트 메소드들 공통 준비 작업과 정리 작업을 처리할 수 있다.

개발을 하면서 테스트 코드가 중요하다는 이야기는 많이 들어 보았지만 정작 테스트 코드를 짜면서 개발을 진행한 적이 없는 것 같다. 이번 기회에 JUnit 의 유용한 사용법들을 알게 되면서 TDD를 적용한 개발을 다음에는 해보고 싶다! 🙌

profile
Believe you can, then you will✨

0개의 댓글