본문 바로가기
  • 실행력이 모든걸 결정한다
Spring Series/Spring Framework

[Spring] 테스트 코드의 작성

by 김코더 김주역 2022. 3. 10.
반응형

1. 테스트 소개

- 테스트는 자신이 예상하고 의도했던 대로 코드가 정확히 동작하는지를 확인하는 것이다.

- Spring은 편리하고 빠른 테스트에 큰 가치를 두고 있다.

- 구현한 기능을 직접 동작시키는 방법으로 테스트를 한다면 반드시 필요한 모든 기능들이 구현이 되어 있어야 하는데, 작은 단위의 테스트를 진행해주면 에러가 났을 때의 위치도 파악하기 쉬워지고, 에러는 없지만 기능이 제대로 동작하지 않는 경우에도 문제의 원인을 찾기가 수월해진다.

- 실제 개발 소스코드와 테스트 소스코드를 구조적으로 분리하면 개발에도 큰 도움이 될 것이고, 코드를 작성하면서 머릿속에서 이루어졌던 테스트들을 테스트 코드로 구현하여 로직 작성에 더욱 집중할 수 있게 된다.

- 다양한 조건에 따른 테스트가 가능하기 때문에 오류를 발생시키는 값의 범위를 분석할 수 있다.

- 테스트 코드를 작성해두면 프레임워크나 제품을 업그레이드 했을 때 새로운 버그가 발생하는지를 테스트할 수 있다.

 

 

 

2. 테스트 프레임워크의 장점

- 일정한 패턴을 가진 테스트를 만들 수 있다.

- 많은 테스트를 간단히 실행시킬 수 있다.

- 테스트 결과를 종합해서 확인할 수 있다.

 

 

 

3. JUnit

1) JUnit 소개

- Java로 테스트를 만들 때 유용하게 쓰이는 Java Testing Framework이다.

- 테스트를 패키지 단위로 실행하면 한 번에 여러 JUnit 테스트를 실행할 수도 있다.

- SpringBoot에서는 spring-boot-starter-test 디펜던시만으로 의존성을 모두 가질 수 있다.

 

 

2) JUnit의 사용

- 테스트 메소드는 public으로 선언돼야 하고, @Test 어노테이션을 붙여야 하고, 리턴형은 void형이어야 하고, 파라미터는 없어야 한다.

- 여러 종류의 assert 메소드들을 제공하며, assert 메소드가 true를 반환하면 넘어가고, false를 반환하면 결과값이 다른 부분을 콘솔창에 표시해준다.

- JUnit 플러그인을 이용해서 테스트 실행 결과를 옵션에 따라 HTML이나 텍스트 파일의 형태로 확인할 수 있다.

 

 

3) JUnit의 주요 assert 메소드

- assertThat(a, matcher) : 가장 많이 사용되는 메소드로, 비교 대상 a를 matcher라는 조건으로 비교한다. org.hamcrest.CoreMatchers 클래스에 선언된 메소드를 JUnit의 matcher로 사용할 수 있다.

※ 다양한 matcher들 모음 : https://beomseok95.tistory.com/250

- assertEquals(a, b) : 객체 a, b가 일치함을 확인한다.

- assertSame(a, b) : 객체 a, b가 같은 객체임을 확인한다.

- assertTrue(a) : 조건 a가 true인가를 확인한다.

- assertNotNull(a) : 객체 a가 null값을 가지지 않음을 확인한다.

- assertArrayEquals(a, b) : 배열 a, b가 일치함을 확인한다.

 

예시

필요한 JUnit import

import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;

assertEquals 사용

@Test
public void testExample() throws Exception {
    String message="Hello, JUnit!";
    assertEquals("Hello, JUnit!", message); //true
    assertEquals("Hello, xUnit!", message); //false
}

 

assert 메소드가 false를 반환하는 경우에는 다음과 같이 콘솔에 예상값과 결과값이 다름을 표시해주고, 어디에서 에러가 발생했는지 보여준다.

 

 

4) 예외에 대한 테스트

- @Test 어노테이션의 expected 속성에 발생할 것으로 기대하는 예외 클래스를 지정하면 한다.

- 아래 코드의 경우에는 테스트 메소드에서 NullPointerException 예외가 발생하지 않으면 테스트는 실패한 것이다.

@Test(expected=NullPointerException.class)
public void testExample(){
	...
}

 

 

5) @Before, @After

- @Before : 각 테스트 메소드를 실행하기 전에 먼저 실행되는 메소드를 선언할 때 사용하는 어노테이션

※ Fixture : 테스트에 필요한 정보나 오브젝트를 의미하며, 가능한 @Before 메소드에서 불러오는게 일반적이다.

@After : 각 테스트 메소드를 실행한 후에 실행되는 메소드를 선언할 때 사용하는 어노테이션

- 테스트 메소드마다 진행되는 과정 : [클래스 오브젝트 생성 -> @Before 메소드 실행 -> @Test 메소드 실행 -> @After 메소드 실행]

※ 테스트 메소드마다 클래스 오브젝트를 새로 생성하는 이유는 각 테스트가 독립적으로 서로 영향을 주지 않게 하기 위함이다.

- 테스트 메소드의 일부에서만 공통적으로 사용되는 코드가 있다면, 일반 메소드를 추출하거나 공통 특징을 가진 테스트 메소드들을 테스트 클래스에 모아서 사용하면 된다.

- JUnit5에서는 @BeforeEach, @AfterEach로 바뀌었다.

 

 

6) @BeforeClass, @AfterClass

- @BeforeClass : 테스트 클래스 전체에 걸쳐 최초 한 번만 실행될 메소드를 선언할 때 사용하는 어노테이션

- @AfterClass : 테스트 클래스 전체에 걸쳐 마지막 한 번만 실행될 메소드를 선언할 때 사용하는 어노테이션

- JUnit5에서는 @BeforeAll, @AfterAll로 바뀌었다.

 

 

7) @RunWith

- 테스트 프레임워크의 실행 방법을 확장할 때 사용하는 어노테이션

- 각각의 테스트별로 객체가 오브젝트가 생성되더라도 싱글톤의 ApplicationContext를 보장한다.

- JUnit5에서는 @ExtendWith로 바뀌었다.

 

 

8) @ContextConfiguration

- Spring Bean 설정 파일의 위치를 설정할 때 사용하는 어노테이션

- 실제 서비스에 영향이 가지 않도록 테스트 전용 설정파일을 따로 만들어 사용하는 것이 안전하다.

 

종합 예시

@RunWith(SpringJUnit4ClassRunner.class) // 테스트가 사용할 ApplicationContext를 만들고 관리하는 작업을 할 수 있도록 JUnit의 기능을 확장
@ContextConfiguration(locations="/test-application.xml")
public class UserDaoTest {

    @Autowired
    private ApplicationContext context;
    
    private UserDao dao;
    
    @Before //가장 먼저 실행되는 테스트 메소드
    public void getUserDao() {
        this.dao = context.getBean("userDao", UserDao.class);
    }
    
    ...
    
    @Test
    public void testMethod1() {
    	// getUserDao()에서 주입 받은 dao를 이용 가능
    }
    
    @Test
    public void testMethod2() {
    	// getUserDao()에서 주입 받은 dao를 이용 가능
    }
}

 

 

9) fail 메소드

- 무조건 테스트를 실패하게 하는 메소드로, 필요에 따라 메시지를 담을 수도 있다.

try {
    // 예외를 반환하기를 기대하는 메소드 호출
    fail("원래는 ~~~라는 예외가 떴어야 했는데...");
} catch (Exception e) {
    // 테스트 성공
}

 

 

 

4. 테스트시 주의할 점

- 테스트에서 @Autowired로 Spring Bean을 사용하는 경우에는 각별한 주의가 필요하다. Spring Bean은 싱글톤으로 관리되기 때문에 내부 변수들이 공유되기 때문이다. 즉, 테스트는 항상 Spring 컨테이너 없이 수행하는 것을 우선적으로 고려해야 한다.

- 일반 ApplicationContext 설정 파일을 테스트에 사용하면 실제 환경에 영향이 갈 수 있기 때문에, 테스트용으로 따로 만들어 사용하는 것이 안전하다.

- @DirtiesContext 어노테이션을 클래스 또는 메소드에 추가해줌으로써, 실제 환경에 영향이 가지 않도록 해당 클래스 또는 메소드에는 ApplicationContext 공유를 허용하지 않는 방법도 있다. @DirtiesContext 어노테이션은 일부러 예외적인 상황을 만들거나 설정파일의 DI 구조를 강제로 바꿔가면서 테스트해야 할 때 사용된다.

 

 

 

5. 테스트 관련 용어들

1) 테스트 주도 개발(TDD)

- 테스트 코드를 미리 작성해두고, 이 테스트 코드를 기능 정의서로 삼아서 실제 코드를 작성하는 방법

- 실패한 테스트를 성공시키기 위한 목적이 아닌 코드는 만들지 않는다는 원칙을 따른다.

- 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기는 가능한 짧게 잡는 것이 바람직하다.

 

 

2) 테스트 스텁

- 테스트 대상 오브젝트의 의존 객체로 존재하면서 테스트 동안에 코드가 정상적으로 수행될 수 있도록 돕는 객체다.

- 테스트 대상 오브젝트가 필요로 하는 입력값과 반환값을 제공해주는 방식으로 서포트 해준다.

- 일반적으로 테스트 코드 내부에서 DI를 통해 간접적으로 사용된다.

 

 

3) 목(Mock) 오브젝트

(1) 목 오브젝트란?

- 테스트 대상 오브젝트의 의존 객체로 존재하면서 테스트를 돕는다는 점은 테스트 스텁과 유사하지만, 테스트 대상 오브젝트가 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증할 수 있는 객체다.

- 테스트 대상과 의존 오브젝트 사이에 주고받는 정보를 저장해둘 수 있다.

 

목 오브젝트 예시

아래 코드의 MockUserDao 클래스는 DB에 접근하지 않고도 테스트 대상의 코드가 정상적으로 수행되도록 도와주고 부가적인 검증 기능까지 갖추었으며, MockUserDao는 UserDao를 대신하는 오브젝트이기 때문에 UserDao를 구현했다.

public class MockUserDao implements UserDao {
    private List<User> users;
    private List<User> updated = new ArrayList<>();

    public MockUserDao(List<User> users) {
        this.users = users;
    }

    public List<User> getAll() {
        return this.users;
    }

    public void update(User user) {
        this.updated.add(user);
    }

    public List<User> getUpdated() {
        return this.updated;
    }
    
    // 테스트에 사용되지 않는 메소드는 실수로 사용될 위험이 있으므로 예외를 던지게 한다.
    public User get(String id) {throw new UnsupportedOperationException();}
    public void add(User user) {throw new UnsupportedOperationException();}
    public void deleteAll() {throw new UnsupportedOperationException();}
    public int getCount() {throw new UnsupportedOperationException();}
}

 

(2) 목 프레임워크

- 대표적으로 Mockito(org.mockito)가 있다.

- 목 프레임워크를 사용하면 목 클래스를 일일이 준비할 필요가 없다.

- 메소드의 호출 기록을 자동으로 남겨준다.

- static import를 사용하여 로컬 메소드처럼 호출할 수 있게 하면 편리하다.

- ArgumentCaptor 클래스를 사용하면 인자값을 검증할 수 있다.

 

목 프레임워크 예시

// UserDao.class를 구현한 목 오브젝트 생성
UserDao mockUserDao = mock(UserDao.class);

// 테스트 도중에 mockUserDao의 getAll() 메소드가 호출됐을 때, this.users를 리턴하도록 설정
when(mockUserDao.getAll()).thenReturn(this.users);

// User 타입의 오브젝트를 파라미터로 받는 mockUserDao의 update() 메소드가 3번 호출됐는지를 확인
verify(mockUserDao, times(3)).update(any(User.class));

// users.get(2)을 파라미터로 mockUserDao의 update() 메소드가 호출된 적이 있는지를 확인
verify(mockUserDao).update(users.get(2));

※ any() 메소드를 사용하면 파라미터의 내용은 무시하고 호출 횟수만 확인할 수 있다.

 

 

 

6. Rollback 테스트

- 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 rollback 하는 테스트

- 어떻게 테스트를 하던간에 어차피 마지막에 rollback 되기 때문에 초기화, 변경, 예외 등에 대한 테스트가 자유로워진다.

- @Transactional 어노테이션을 테스트에서 사용하면 테스트가 끝났을 때 자동으로 rollback을 수행해준다.★

 

 

 

● 참고 자료 : 토비의 스프링 3.1

반응형

댓글