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

2023. 11. 22. 02:07Books

2.1 UserDaoTest 다시 보기

2.1.1 테스트의 유용성

앞서 main 메소드를 통해서 작성한 코드가 잘 동작하는지 확인했다. 하지만 1장에서 다양한 리팩토링을 거치며 예상한 의도와 코드가 정확히 일치하는지 확인하기 위해서는 테스트를 작성하는 것이 좋다. 테스트의 결과가 원하는 대로 나오면 코드의 변경 이후에도 설계나 결함이 없다고 확신을 얻을 수 있기 때문이다.

2.1.2 UserDaoTest의 특징

Django를 이용하여 다양한 프로젝트를 진행하며 제대로 된 테스트를 짜본 적이 없다. 이에 테스트를 하려면 서버를 켜고 직접 값을 넣으며 제대로 작동하는지 확인을 해야 했다. DB를 한번 비우면 테스트를 하기 위한 노가다 작업이 필수였고, 그 작업을 하던 와중에도 오류가 발생할 가능성이 있었다. 이에 어느 부분에서 정확히 오류가 나는지 확인하지 못하는 경우도 허다했다. 이러한 문제를 해결하기 위해 책에서는 작은 단위의 테스트를 하는 것이 바람직하다고 한다. 또한 작은 단위의 테스트에는 관심사의 분리라는 원리도 적용된다.

 

-> 작은 단위의 테스트 == 단위 테스트 (Unit Test)

 

 단위 테스트

  • 단위 테스트는 통제할 수 없는 외부의 리소스에 의존하면 안 된다.
  • 설계한 코드가 원래 의도한대로 동작하는지 개발자 스스로 빨리 확인받기 위해서이다.
  • 확인의 대상, 조건이 간단하고 명확할수록 좋다
  • 점진적인 개발이 가능하다

2.1.3 UserDaoTest의 문제점

  • 수동 확인 작업의 번거로움
    • 콘솔로 결과를 출력해줄 뿐이지, 테스트가 성공했는지, 실패했는지는 사용자가 직접 확인해야 한다.
  • 실행 작업의 번거로움
    • 테스트양이 많아지면 main 메소드를 그만큼 많이 수행하는 수고가 필요하다.

2.2 UserDaoTest 개선

2.2.1 테스트 검증의 자동화

테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것
- 켄트 백

코드의 기능 모두 점검할 수 있는 포괄적인 테스트를 통해 유지보수를 하면서 기존 코드 수정을 하더라도 마음의 평안을 얻고, 자신이 만지는 코드에 대해 항상 자신감을 가질 수 있다.

-> 자동화 테스트를 만들자.

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

JUnit : 자바 테스팅 프레임워크

  • 메소드가 public으로 선언돼야 한다. (생략 가능)
  • 메소드에 @Test라는 애노테이션을 붙여준다.
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("geumjang");
        user.setName("안금장");
        user.setPassword("1234");
        
        dao.add(user);
        
        User user2 = dao.get(user.getId());
        
        assertThat(user2.getName(), is(user.getName()))
        assertThat(user2.getPassword(), is(user.getPassword()))
    }
}

이러한 테스트 코드를 통해 테스트의 성공, 실패를 알 수 있다.

2.3 개발자를 위한 테스팅 프레임워크 JUnit

현재 위의 코드처럼 작성하면 두 번째 테스트를 실행하면 DB에 중복된 User가 이미 존재하기 때문에 테스트가 실패할 것이다. 테스트가 외부 상태에 따라 성공하기도 하고 실패하기도 한다. 이에 DB를 비우는 메소드를 추가로 작성하여 테스트 시작 전에 넣어 준다. 이후부터는 테스트를 여러 번 돌려도 성공한다. 하지만 여러 가지 DB를 이용하는 메소드를 테스트하면 오류가 날 수도 있다.

 

JUnit은 예외상황에 대한 테스트도 제공한다. 교재에서는 @Test(expected=~~Exception.class)로 설명하지만 JUnit5 부터는 assertThrows 등을 이용하여 테스트할 수 있다.

포괄적인 테스트

테스트는 성공하는 테스트만 작성하지 않고 다양한 상황과 입력 값을 고려하는 포괄적인 테스트를 만들어야 한다. 이에 부정적인 케이스를 먼저 만드는 습관을 들이는 게 좋다. 예를 들어 id를 통해 User를 가져오는 메소드를 테스트할 때 존재하는 id를 줘서 가져오는 것을 테스트하는 것도 중요하지만 존재하지 않는 id가 주어졌을 때 어떻게 반응할지 결정하고, 이를 확인하는 테스트를 만드는 것도 중요하다.

2.3.4 테스트가 이끄는 개발

테스트를 만들 때

  1. 조건
  2. 행위
  3. 결과

를 토대로 작성한다. given / when / then 으로 표현하기도 한다.

 

만들고자 하는 기능의 내용을 담고 있으면서 만들어진 코드를 검증도 해줄 수 있도록 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발을 테스트 주도 개발 (TDD, Test Driven Development) 라고 한다.

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

 

TDD의 장점

  • 테스트를 빼먹지 않고 꼼꼼하게 만들 수 있다.
  • 테스트를 작성하는 시간과 애플리케이션 코드를 작성하는 시간의 간격이 짧아진다.
  • 코드를 작성하면 바로 테스트를 실행하고 빠른 피드백을 얻을 수 있다.

2.3.5 테스트 코드 개선

테스트 코드도 코드다. 리팩토링을 생각하며 내부구조와 설계를 개선하자.

@Before

메소드 앞에 JUnit이 제공하는 @Before 애노테이션을 두어 @Test 메소드가 실행되기 전에 먼저 실행해야 하는 메소드를 정의할 수 있다. 이는 각 테스트 메소드에 반복적으로 나타났던 코드를 제거하여 별도의 메소드로 옮길 수 있게 해 준다. 이런 식으로 JUnit 프레임워크는 스스로 제어권을 가지고 주도적으로 동작한다.

 

JUnit의 테스트 수행 방식

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

테스트 하나마다 새로운 오브젝트를 왜 만드는가?

-> 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장해 주기 위해서이다.

픽스처

테스트를 수행하는 데 필요한 정보나 오브젝트

여러 테스트에서 반복하기 때문에 @Before 메소드를 이용해 생성해 두면 편하다.

public class UserDaoTest {

    private UserDao dao;
    private User user1;
    private User user2;
    private User user3;

    @Before
    public void setUp() {
        this.user1 = new User("geumjang", "안금장", "1234");
        this.user2 = new User("geumjang2", "안금장2", "1234");
        this.user3 = new User("geumjang3", "안금장3", "1234");
    }

2.4 스프링 테스트 적용

ApplicationContext 같은 경우 생성될 때 시간이 많이 걸리는 경우도 있다. 하지만 한번 초기화되면 내부의 상태가 바뀌는 일이 거의 없고 빈은 싱글톤으로 만들었기 때문에 상태를 갖지 않는다. 이에 ApplicationContext는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다.

@ExtendWith(SpringExtension.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;

    @Before
    public void setUp() {
        this.dao = this.context.getBean("userDao", UserDao.class);

    }

 

책과 다르게 JUnit5를 기준으로 작성했다.

해당 코드를 실행하면 context를 초기화해주지 않았지만 NPE가 일어나지 않고 아무런 문제 없이 성공적으로 끝난다.

테스트 클래스의 컨텍스트 공유

스프링 테스트 컨텍스트 프레임워크는 하나의 테스트 클래스 안에서만 ApplicationContext를 공유해 주는 것이 아니다. 같은 설정파일을 가진 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해 준다.

@Autowired

@Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 있으면 변수에 주입해 준다.(DI) 단, 같은 타입의 빈이 두 개 이상 있는 경우에는 변수의 이름을 통해 확인한다.

테스트를 위한 별도의 DI 설정

DataSource가 테스트용, 운영용이 겹친다면 위험한 일이 벌어질 것이다. 이에 두 가지 종류의 설정파일을 만들어둘 수 있다. 혹은 컨테이너를 사용하지 않고 @Before 메소드에서 datasource를 직접 지정 해줄 수 있다.

 

DI를 테스트에 이용하는 여러 가지 방법이 있지만 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 우선적으로 고려하자.

-> 가장 빠르고 테스트가 간결하다.

2.5 학습 테스트로 배우는 스프링

학습 테스트 : 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리 등에 대한 테스트를 작성

-> API나 프레임워크의 기능을 테스트로 보며 사용 방법을 익힐 수 있다.

2.5.1 학습 테스트의 장점

  1. 다양한 조건에 따른 기능을 손쉽게 확인해 볼 수 있다.
  2. 학습 테스트 코드를 개발 중에 참고할 수 있다.
  3. 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
  4. 테스트 작성에 대한 좋은 훈련이 된다.
  5. 새로운 기술을 공부하는 과정이 즐거워진다.

2.5.3 버그 테스트

버그 테스트 : 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있는 테스트

일단 실패하도록 만든 이후 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정한다.

 

장점

  1. 테스트의 완성도를 높여준다.
  2. 버그의 내용을 명확하게 분석하게 해 준다.
  3. 기술적인 문제를 해결하는 데 도움이 된다.