단위 테스트

작은 단위의 코드에 대해 테스트를 수행한 것

충분히 하나의 관심에 집중해서 효율적으로 테스트할 만한 범위의 단위

테스트의 장점

  • 코드를 수정하고 설계를 개선해나가는 과정에 자신감을 준다.
  • 수정에 실수가 있었다면 테스트를 통해 무엇이 잘못됐는지 바로 확인할 수 있다.

로드 존슨(스프링 만듦) : 테스트를 만들때는 항상 네거티브 테스트를 먼저 만들라.

테스트 클래스의 작성

given → when → then에 맞춰 테스트 코드를 작성하는 게 가독성과 명확성 측면에서 좋다.

Dependency : spring-test, junit-jupiter-api

@ExtendWith(SpringExtension.class)

ExtendWith : JUnit의 확장을 위해 사용하는 애노테이션

SpringExtension : JUnit에 사용되는 스프링 테스트 컨텍스트 프레임워크 확장 클래스 (하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용할 수 있게 해준다)

@ContextConfiguration

애플리케이션 컨택스트의 설정파일의 위치를 지정해준다.

여러 개의 테스트 클래스가 있는데 모두 같은 설정파일을 가진 애플리케이션 컨텍스트를 사용한다면, 스프링은 테스트 클래스 사이에서도 애플리케이션 컨텍스트를 공유하게 해준다. 스프링은 설정파일의 종류만큼 애플리케이션 컨텍스트를 만들고, 같은 설정파일을 지정한 테스트에서는 이를 공유하게 해준다.

@Autowired

  • 이 애노테이션이 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 주입을 위해 생성자나 수정자 메소드가 필요하지만, 이 경우에는 메소드가 없어도 주입이 가능하다. 단, 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로 어떤 빈을 가져올지 결정 할 수 없다. 같은 경우에는 변수의 이름과 같은 이름의 빈이 있는지 확인한다. 변수 이름으로도 빈을 찾을 수 없는 경우에는 예외가 발생한다.
  • 인터페이스 타입의 변수 VS 구현 클래스 타입의 변수 인터페이스 : 구현 클래스를 변경하더라도 테스트 코드를 수정할 필요가 없다. 구현 클래스 : 해당 타입의 오브젝트 자체에 관심이 있는 경우 사용. 테스트는 필요하다면 얼마든지 애플리케이션 클래스와 밀접한 관계를 맺어도 상관없다. 하지만 필요하지 않다면 가능한 인터페이스를 이용하자. 예) DB 연결정보 확인, 클래스 메소드 테스트 등…
  • 별도의 DI 설정 없이 필드의 타입정보를 이용해 빈을 자동으로 가져오는 것을 타입에 의한 자동와이어링이라고 한다.
  • 스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록한다. 따라서 xml파일에 따로 ApplicationContext타입의 빈을 등록하지 않아도 빈이 존재하는 셈이고 DI도 가능한 것이다.
@ExtendWith(SpringExtension.class) //JUnit용 테스트 컨텍스트 프레임워크 확장 클래스
@ContextConfiguration("/applicationContext.xml") // 자동으로 만들어줄 애플리케이션 컨택스트의 설정파일 위치
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
    private UserDao dao;
    private User user1, user2, user3;

    @BeforeEach
    public void setUp() {
        System.out.println(context);
        System.out.println(this);
        this.dao = context.getBean("userDao", UserDao.class);

        this.user1 = new User("a", "aname", "apass");
        this.user2 = new User("b", "bname", "bpass");
        this.user3 = new User("c", "cname", "cpass");
    }

@DirtiesContext

애플리케이션 컨텍스트의 구성이나 상태의 변경을 위해 사용하는 애노테이션

@DirtiesContext // 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 
//테스트 컨텍스트 프레임워크에 알려준다.
// 이 애노테이션이 붙은 테스트 클래스는 애플리케이션 컨텍스트의 공유를 허용하지 않는다.
// 테스트 메소드 마다 매번 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다.
// 메소드 레벨에 적용이 가능하므로 하나의 메소드에만 적용하는 편이 좋다.
public class UserDaoTest {
    @Autowired
    private UserDao dao;
    private User user1, user2, user3;

    @BeforeEach
    public void setUp() {
        // UserDao가 테스트에 사용할 DataSource 오브젝트를 사용한다.
        // SingleConnectionDataSource는 하나의 커넥션만 만들어 사용해 매우 빠르다.
        DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);
        // 수동 DI
        dao.setDataSource(dataSource);
        this.user1 = new User("a", "aname", "apass");
        this.user2 = new User("b", "bname", "bpass");
        this.user3 = new User("c", "cname", "cpass");
    }

JUnit 테스트의 특징

  • 테스트 클래스 오브젝트는 매 테스트 함수를 호출할 때마다 새로 만들어진다. 단, 테스트 클래스와 함께 동작하는 확장된 스프링 테스트 컨테이너는 한 번만 만들어진다.
  • @BeforeAll 을 이용해 해당 테스트 클래스의 모든 테스트 함수 실행 전 필요한 전처리를 할 수 있다.

목 오브젝트를 통한 테스트

목 프레임워크

단위 테스트를 만들기 위해서 스텁이나 목 오브젝트의 사용이 필수적이다. 단위 테스트가 많은 장점이 있고 가장 우선시 할 테스트 방법이지만 작성이 번거롭다. 특히 목 오브젝트를 만드는 일이 가장큰 짐이다. 이런 번거로운 목 오브젝트의 작성을 편리하게 도와주는 목 오브젝트 지원 프레임워크를 알아보자.

Mockito 프레임워크

목 프레임워크의 특징은 목 클래스를 일일이 준비해둘 필요가 없다는 점이다. 간단한 메소드 호출로 다이내믹하게 특정 인터페이스를 구현한 테스트용 목 오브젝트를 만들 수 있다.

목 오브젝트는 다음의 네 단계를 거쳐서 사용하면 된다.

  1. 인터페이스를 이용해 목 오브젝트를 만든다.
  2. 목 오브젝트가 리턴할 값이 있으면 이를 지정해준다. 메소드가 호출되면 예외를 강제로 던지게 만들 수 있다.
  3. 테스트 대상 오브젝트에 DI 해서 목 오브젝트가 테스트 중에 사용되도록 만든다.
  4. 테스트 대상 오브젝트를 사용한 후에 목 오브젝트의 특정 메소드가 호출됐는지, 어떤 값을 가지고 몇 번 호출됐는지를 검증한다.

목 오브젝트는 테스트 중 호출되면 자동으로 호출 기록과 파라미터를 기록한다. 미리 설정된 리턴값이 있는 경우에는 그 값을 그래로 리턴해주기도 한다.

호출 횟수 검사는 다음과 같다. (times()는 메소드 호출 횟수를 검증해준다. any() 메소드는 파라미터의 내용은 무시하고 호출 횟수만 확인할 수 있다.)

verify(mockUserDao, times(2)).update(any(User.class));

호출 횟수 검사가 끝나면 목 오브젝트가 호출됐을 때의 파라미터를 하나씩 점검한다. 다음 코드는 users.get(1)을 파라미터로 update가 호출된 적 있는지를 확인한다. update()가 호출된 적 없거나 파라미터가 users.get(1)이 아니면 테스트는 실패한다.

verify(mockUserDao).update(users.get(1));

오브젝트는 확인했지만 레벨의 변화는 파라미터의 직접 비교로는 확인되지 않는다. 따라서 getAll()을 통해 전달했던 this.users의 내용 변화를 확인해야 한다.

MailSender의 경우 ArgumentCaptor 클래스를 활용해 실제 MailSender 목 오브젝트에 전달된 파라미터를 가져와 내용을 검증하는 방법을 사용했다. 위의 update메소드를 검증 할 때 처럼 파라미터를 직접 비교하기보다는 파라미터의 내부 정보를 확인해야하는 경우 유용하다.

ArgumentCaptor<SimpleMailMessage> mailMessageArg = ArgumentCaptor.forClass(SimpleMailMessage.class);

verify(mockMailSender, times(2)).send(mailMessageArg.capture());

List<SimpleMailMessage> mailMessages = mailMessageArg.getAllValues();
assertEquals(users.get(1).getEmail(), Objects.requireNonNull(mailMessages.get(0).getTo())[0]);
assertEquals(users.get(3).getEmail(), Objects.requireNonNull(mailMessages.get(1).getTo())[0]);

출처

토비의 스프링(책)

댓글남기기