Spring - (24) : 스프링과 테스트코드

­이승환·2021년 12월 17일
0

spring

목록 보기
21/26

Overview


이번장에선 TDD와 스프링에 대해서 요약해보고자 한다. 앞선 포스팅에서 처럼 토비의 스프링을 읽고 요약한 자료이다.

문제가 있을 시 lshn1007@hanyang.ac.kr 로 메일 주시면 삭제하겠습니다.

기능을 전부 구현한 후에 테스트를 하면 생기는 문제점

보통 테스트를 한다고 하면 애플리케이션에 필요한 기능을 대충이라도 모두 구현한 후 테스트를 하는데 이 방법은 문제점이 많다. 알고리즘 문제에서 만일 testcase가 주어지지 않는다면.. 당신은 정답이라 확신할 수 있는가?

  • 모든 레이어의 기능을 다 만들고 나서야 테스트가 가능하다.
  • 테스트를 하고자 하는 대상에 다른 계층의 코드, 설정이 영향을 줄 수 있기 때문에 어느 부분에서 문제가 발생했는지 확인하기가 어렵다.

이러한 단점이 발생한 원인은 한번에 많은 것을 몰아서 테스트를 했기 때문이다. 이렇게 할 경우 테스트 수행 과정도 복잡해지고, 오류가 발생했을 때 정확한 원인을 파악하기가 쉽지 않다. 따라서 테스트는 가능하면 작은 단위로 쪼개서 수행해야 한다. 이 때 말하는 단위란, 명확히 정의된 단위가 아니고, 테스트가 오로지 하나의 관심에 집중될 수 있는 경우 이를 하나의 단위라고 말할 수 있다.

단위 테스트

작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(Unit test) 라고 하는데, 일반적으로 단위는 작을수록 좋으며 단위의 범위를 넘어서는 다른 코드들은 신경쓰지 않고, 테스트가 동작할 수 있으면 좋다. 단위 테스트는 개발 후에 기능 오류를 빠르게 확인해볼 수 있고, 개발자가 작성한 코드가 원래 의도한대로 동작하는지를 스스로 확인해볼 수 있기 때문에 반드시 필요하다.

개별로 단위 테스트를 수행해도 좋고, 통합 테스트를 최종적으로 진행하는 것도 좋다. 다만 Spring의 경우 DB server와 연결되는 경우도 있기 때문에 단위를 잘 고려해야 하는 것이 좋다.

자동 수행 테스트 코드

사용자 수동 테스트

애플리케이션을 직접 구동하고 값을 입력하여 기능이 정상적으로 동작했는지 눈으로 확인하는 것과 같이 테스트를 수동으로 진행하는 것은 문제가 많다.

  • 수동 확인 작업의 번거로움
  • main 메소드를 통해 테스트를 수행할 경우, 각각의 테스트마다 main 메소드가 존재하기 때문에 전체 테스트를 위해서는 매번 main 메소드를 실행시켜줘야 한다.

JUnit Framework 테스트

반면에 테스트 코드를 작성하여 테스트를 수행하면 장점이 많다.

  • 코드를 수행시키기만 하면 자동으로 테스트가 수행되므로 테스트 수행 속도가 빠르다.
  • 작성한 테스트 코드는 재사용이 가능하므로 자주 반복하여 수행할 수 있다.
  • 작성해둔 테스트 코드를 이용해 기능을 수정했을 때 다른 기능에 사이드 이펙트가 발생했는지 빠르게 확인할 수 있다.
  • 개발자가 스스로 작성한 코드에 대해 확신을 가질 수 있다.

테스트 코드 작성 시 유의사항

  1. 코드에 변경 사항이 없다면 단위 테스트는 항상 일관성 있는 결과가 보장되어야 한다.

테스트는 외부 환경, 테스트 실행 순서에 영향 없이 항상 동일한 결과가 보장되어야 한다. 항상 동일한 결과를 보장하도록 테스트를 만드는 방법은 아래와 같다.

  • 테스트 실행 전에 테스트 실행에 문제가 되지 않는 상태를 만들어준다.
  • 테스트 이후에 테스트 과정에서 변경, 추가된 데이터를 테스트 이전 상태와 같이 만들어준다.
  1. 테스트 케이스에서는 한 가지 결과만 검증해서는 안된다.

개발한 코드에 문제가 있음에도 운이 좋게 테스트 케이스에서 성공하는 경우(=못해도 하루에 두 번은 맞는다는 죽은 시계)가 있을 수 있다. 따라서 한 가지 결과만 가지고 검증하지 말고, 가급적이면 여러 경우를 대입해 검증해보는 것이 좋다. 즉 동일 단위 테스트에 대해 다양한 검증을 시도하면 할 수록 좋다.

  1. 테스트 케이스는 한번에 한 가지 검증 목적에만 충실해야 한다.
  1. 테스트 케이스를 만들 때는 부정적인 케이스로 만들어라.

개발자가 테스트를 직접 만들 때 자주 하는 실수가 하나 있다. 바로 성공하는 테스트만 골라서 만드는 것이다. 이렇게 할 경우 코드에서 향후 발생할 수 있는 다양한 예외 상황을 살펴보지 못하고 넘어갈 수 있기 때문에 테스트 케이스를 만들 때는 항상 네거티브 케이스를 먼저 만드는 습관을 들이는 게 좋다.

JUnit 프레임워크

  • JUnit 프레임워크는 독립된 단위 테스트를 지원하는 자바 테스팅 프레임워크를 말한다
  • JUnit 테스트는 main(), System.out.println()으로 만든 테스트만큼 단순하기 때문에 빠르게 테스트 코드를 작성할 수 있다.
  • JUnit 프레임워크는 테스트 코드 작성 시 다양하고 편리한 부가 기능을 제공한다.
  • 대부분의 자바 IDE는 JUnit 테스트 지원 기능을 내장하고 있어 편리하게 활용할 수 있다. (인텔리제이 짱짱)

테스트 코드 작성 방법

  • 테스트 클래스를 만들고, 클래스 안에 테스트 메소드를 만들면 된다.

  • 이 때, 테스트 메소드는 JUnit 프레임워크가 요구하는 조건 2가지를 준수하여 작성해야 한다.

    	(1) 메소드의 접근 제어자를 public, 반환형은 void로 선언해야 한다. (JUnit에서는 public메소드만을 테스트 메소드로 허용)
    	(2) 메소드에 @Test 어노테이션을 붙여줘야 한다.
  • 규칙은 아래와 같다.

테스트 클래스 명명 규칙 : JUnit에서 사용할 테스트 클래스는 관례에 따라 '테스트할 클래스명 + Test' 와 같이 명명한다.

테스트 메소드 명명 규칙 : 테스트 메소드의 이름은 테스트 의도가 무엇인지 알 수 있는 이름으로 명명하는 것이 좋다. @DisplayName 을 활용하는 것도 방법이 될 수 있다.

public class UserDaoTest {
        // @Test : JUnit에게 테스트용 메소드임을 알려준다.
	@Test   
	public void addAndGet() throws SQLException {
		ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
		UserDao dao = context.getBean("userDao", UserDao.class);
		...
	}
}

JUnit이 테스트를 수행하는 과정

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

왜 테스트 메소드를 실행할 때마다 새로운 오브젝트를 생성할까?
각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장하기 위함이다.

픽스처(fixture)란?
테스트를 수행하는 데 필요한 정보나 오브젝트를 픽스처라고 한다. 일반적으로 픽스처는 여러 테스트에서 반복적으로 사용하므로 @BeforeEach 어노테이션을 이용해 생성하거나 DI를 이용하면 편리하다.

assertThat()

  • JUnit 에서 지원하는 static method로 메소드에 들어간 두 파라미터의 값을 비교할 수 있다.
  • 첫번째 파라미터에는 비교대상 값을, 두번째 파라미터로는 비교 로직이 담긴 Matcher가 사용된다. (비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 AssertionError를 발생시킨다.)
  • assertThat과 Matcher의 조합을 활용하면 검증을 수행하기 위한 다양한 형태의 비교 로직을 만들 수 있다.
import org.junit.Test;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.*;

public class UserControllerTest {
    @Test
    public void getUser() {
        assertThat(true, is(false));  // Test failed    
    }
}

Matcher method

  • is() : param1과 param2를 equals()로 비교한다.

  • not() : assertThat()에서 뒤에 나오는 matcher를 통해 비교한 결과를 반대로 뒤집는다.

  • sameInstance() : param1과 param2의 동일성을 확인한다.

  • hasItem() : param1 collection에 param2가 포함되어 있는지 확인한다.

  • nullValue() : param1이 null인지 확인한다.

  • either() : either A or B 형식으로 matcher를 사용할 수 있게 해준다. (A, B 매쳐 둘 중 하나가 성공할 경우 테스트 성공)

JUnit annotation (JUnit4기준)

@Test

  • 이 어노테이션을 메소드에 사용하면, 해당 메소드는 테스트를 수행하는 메소드가 된다.

@Before

  • 메소드에 사용할 수 있으며, 클래스 내의 테스트 메소드가 실행되기 전에 매번 호출된다.

@After

  • 메소드에 사용할 수 있으며, 클래스 내의 테스트 메소드가 실행된 후에 매번 호출된다.

@BeforeClass

  • static 메소드에 사용할 수 있으며, 모든 테스트 메소드가 실행되기 전에 테스트 클래스 전체에 걸쳐 딱 한 번만 실행된다.
  • 객체를 공유하여 사용하는게 유리한 경우(ex. 애플리케이션 컨텍스트와 같이 객체를 한번 생성할 때 많은 시간과 자원이 소모될 수 있는 경우)에 사용할 수 있다.

@AfterClass

  • static 메소드에 사용할 수 있으며, 모든 테스트 메소드의 테스트가 완료된 후에 딱 한 번만 실행된다.

@RunWith(SpringJUnit4ClassRunner.class)

  • JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 어노테이션이다.
  • SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크 확장 클래스를 지정해주면 JUnit이 테스트를 진행하는 중에 테스트가 사용할 ApplicationContext를 만들고 관리하는 작업을 진행해준다.
  • 이 어노테이션은 각각의 테스트 별로 객체가 생성되더라도 ApplicationContext가 하나만 생성되도록 해준다. (단, 테스트 클래스가 사용하는 설정 파일이 같은 경우에만 동일한 ApplicationContext를 사용한다. → @ContextConfiguration 에서 설정한 파일이 같아야 함)

@ContextConfiguration

  • Spring Bean 설정 파일의 위치를 지정할 때 사용하는 어노테이션이다.
  • applicationContext를 로딩할 때 XML 설정 파일을 사용하여 로딩하는 경우 사용할 수 있다.
  • Spring은 설정 파일의 종류만큼 ApplicationContext를 만들고, 같은 설정 파일을 지정한 테스트에서는 동일한 ApplicationContext를 공유할 수 있게 해준다.
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "/applicationContext.xml")
public class UserControllerTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void getUser() {
        User user = applicationContext.getBean(User.class);
        assertThat("beanUserId", is(user.getId()));
        assertThat("beanUserPassword", is(user.getPassword()));
    }
}

기타 Spring Annotation

@DirtiesContext

  • 클래스, 메소드 레벨에 사용 가능한 어노테이션으로, 클래스에 지정하면 이 클래스의 테스트 메소드에서 애플리케이션 컨텍스트의 구성이나 상태를 변경한다는 것을 테스트 컨텍스트 프레임워크에 알려준다.
  • 따라서, 매 테스트 메소드가 수행될 때마다 새로운 애플리케이션 컨텍스트를 만들어서 다음 테스트가 사용하게 해준다. (테스트 중에 변경한 컨텍스트가 뒤의 테스트에 영향을 주지 않게 하기 위해서다.
  • 만약 메소드 레벨에 지정한다면, 해당 테스트가 종료될 때 새로운 애플리케이션 컨텍스트를 생성하여 다음 테스트에 이상이 없도록 해준다.)

@Autowired

  • 이 어노테이션이 붙은 인스턴스 변수가 있으면 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다. 일반적으로는 주입을 위해 생성자나 수정자 메소드 같은 메소드가 필요하지만, 이 경우에는 메소드가 없어도 주입이 가능하다.
  • 또 별도의 DI 설정없이 필드의 타입 정보를 이용해 빈을 자동으로 가져올 수 있는데, 이런 방법을 타입에 의한 자동와이어링이라 한다.
  • 단, 같은 타입의 빈이 두 개 이상 있는 경우에는 타입만으로는 어떤 빈을 가져올지 결정할 수 없으므로 변수의 이름과 같은 이름의 빈이 있는지 확인한다. 만약 변수 이름으로도 빈을 찾을 수 없는 경우에는 예외가 발생한다.
  • 여기에 활용할 수 있는 어노테이션은 @Qualifier Primary 가 있는데 이전 포스팅을 참조하자.

예외 테스트

JUnit에서는 예외 상황에 대한 테스트를 수행해볼 수 있다. @Test 어노테이션의 expected 속성의 값을 테스트 중에 발생할 것으로 기대되는 예외 클래스로 설정해주면 된다. 이렇게 하면 테스트 결과로 예외가 던져지지 않을 경우 테스트 결과는 실패가 된다. 최근에는 assertThrow를 이용해 Build패턴으로 예제가 많은 듯 하니 참고하자! 토비의 스프링은 12년도에 출판된 도서이다.

@SpringBootTest
@RunWith(value = SpringJUnit4ClassRunner.class)
//@ContextConfiguration(locations = "/applicationContext.xml")
public class UserControllerTest {

    @Autowired
    private UserController userController;

    // 테스트 중에 발생할 것으로 예상되는 예외 기입
    @Test(expected = NullPointerException.class)
    public void getNullPointerException() {
        userController.getNullPointerException();
    }
}

테스트를 위한 애플리케이션 컨텍스트 관리

Spring application의 기능을 테스트하기 위해 애플리케이션 컨텍스트를 @Before 메소드로 생성하려고 하면 매 테스트 메소드가 실행되기 전 애플리케이션 컨텍스트가 생성하므로 application의 규모가 커지면 커질수록 애플리케이션 컨텍스트 생성에 많은 시간과 자원이 소모될 수 있다.

본래 테스트는 가능한 한 독립적으로 매번 새로운 오브젝트를 만들어서 사용하는 것이 원칙이지만, 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.

따라서, 이를 위해 JUnit에서 제공하는 @BeforeClass 를 이용해 static 필드로 선언한 애플리케이션 컨텍스트를 초기화하여 사용할 수도 있겠으나, 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 지원 기능을 사용하는 것이 더 편리하다. (@RunWith & @ContextConfiguration)

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {

   /**
    * 테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동으로 값이 주입된다.
    * 스프링 애플리케이션 컨텍스트는 초기화할 때, 자기 자신도 Bean으로 만들기 때문에 이러한 동작이 가능하다.
    */
    @Autowired
    private ApplicationContext context;

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

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

테스트 코드에서 운영용 설정 파일을 이용해 DI를 수행하고 테스트 하는 것은 문제가 되기 때문에 테스트 시에는 반드시 별도의 DI 설정 파일을 만들어 사용해야 한다.

기존에 운영용 DI 설정 파일을 applicationContext.xml 이라는 이름으로 만들어 사용했다면, 테스트용 DI 설정 파일은 test-applicationContext.xml 과 같이 다르게 만들어 운영 환경과 테스트 환경에서 사용할 DI 설정 파일을 분리하면 된다. (@ContextConfiguration 어노테이션의 locations만 테스트 환경의 설정 파일의 이름으로 변경해주면 된다.)

테스트 코드 개선

애플리케이션 코드 뿐만 아니라 테스트 코드도 리팩토링의 대상이 될 수 있다. 만약, 여러 테스트 메소드에서 중복된 코드가 존재할 경우 아래와 같이 리팩토링이 가능하다.

  • 중복 코드가 테스트의 맨 앞부분 혹은 맨 뒷부분에 있을 경우 JUnit의 @Before 나 @After 를 사용하여 테스트 메소드 실행 전,후에 중복된 코드가 실행될 수 있도록 메소드로 분리한다.
  • 테스트 메소드의 일부에서만 중복 코드가 있다면, 테스트 메소드에서 직접 호출해 사용할 수 있도록 일반적인 메소드 추출 방법을 사용해 메소드를 분리한다.
  • 테스트하기 위한 클래스가 있다면 inner class로 선언해서 사용해보자.

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

  • 테스트 주도 개발이란, 테스트 코드를 먼저 만들고, 테스트가 성공하도록 코드를 작성하는 방식의 개발 방법을 말한다.
  • 이 때 테스트 코드는 개발하고자 하는 기능의 내용을 담고 있어야 한다. (일종의 기능 정의서)
  • TDD에서는 테스트를 작성하고 이를 성공시키는 코드를 만드는 작업의 주기를 가능한 짧게 가져가도록 하는 것을 권장한다.
  • 기능을 먼저 개발하고 테스트 코드를 작성하게 되면 테스트 작성 과정이 귀찮아지고 성의 없는 테스트로 전락하기 쉽지만, 테스트 코드를 먼저 개발하면 테스트를 빼먹지 않고 꼼꼼하게 작성해볼 수 있다.
  • 개발한 코드에 대해 피드백을 바로 받아볼 수 있어 오류를 쉽고 빠르게 찾을 수 있다.
    테스트 코드를 통해 개발한 코드를 더욱 확신할 수 있게 된다.

학습 테스트(learning test)

  • 자신이 만들지 않은 프레임워크, 라이브러리 등에 대해 학습을 목적으로 작성해보는 테스트를 말한다.
  • 학습 테스트를 작성하는 목적은 작성한 코드를 검증하기 위한 목적이 아니라, 프레임워크나 라이브러리의 기능을 직접 테스트 코드로 작성해보면서 사용 방법을 익히기 위함이다.
  • 다양한 조건에 따라 기능이 어떻게 동작하는지 빠르게 확인할 수 있다.
    작성한 학습 테스트 코드를 개발 중에 참고할 수 있다.
  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
  • 테스트 작성에 대한 좋은 훈련이 된다.

버그 테스트

  • 코드에 오류가 있을 때 그 오류를 가장 잘 드러내줄 수 있도록 테스트 코드를 작성하는 것을 말한다.
  • 따라서 테스트 코드는 해당 버그가 원인이 되서 일단 실패하도록 작성해야 한다.
  • 그리고 버그 테스트가 성공할 수 있도록 애플리케이션 코드를 수정하여 테스트에 성공하면 해당 버그는 해결된 것이다.
  • 기존 테스트를 보완하여 테스트의 완성도를 높여준다.
  • 버그의 내용을 명확하게 분석할 수 있도록 해준다.

기타

침투적 기술과 비침투적 기술

  • 침투적(invasive) 기술은 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장하거나, 특정 인터페이스나 클래스를 사용하도록 강제하는 기술을 말한다. 따라서 애플리케이션 코드가 해당 기술에 종속되는 결과를 가져온다.
  • 반면에 비침투적(noninvasive) 기술은 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. 따라서 기술에 종속적이지 않은 순수한 코드를 유지할 수 있게 해준다.

동등분할(equivalence partitioning)

  • 같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다.
  • 어떤 작업의 결과가 true, false, 예외와 같이 3가지 종류라면 각 결과를 내는 입력값이나 상황의 조합을 만들어 모든 경우에 대한 테스트를 해보는 것이 좋다.

경계값 분석(boundary value analysis)

  • 에러가 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해 경계의 근처에 있는 값을 이용해 테스트하는 방법이다.
  • 보통 숫자가 입력 값인 경우 0이나 그 주변 값 또는 정수의 최대, 최소값 등으로 테스트해보면 도움이 될 때가 많다.
profile
Mechanical & Computer Science

0개의 댓글