⚙️

[토비의 스프링] 2. 테스트에 대해서

개요

스프링이 개발자에게 제공하는 가장 중요한 가치 2가지는 객체지향테스트입니다.

애플리케이션은 계속 변화하며 복잡해집니다.
그 변화에 대응하는 첫번째 전략이 확장과 변환를 고려한 ‘객체지향적 설계’와 ‘IoC/DI’ 같은 기술입니다.
두번째 전략은 변화에 유연하게 대처할 수 있는 것이 ‘테스트 기술’입니다.

올바른 테스트를 만들려면 ?

1. 작은 단위의 테스트

테스트란 개발자가 의도했던 대로 코드가 정확히 동작하는지 확인하는 작업입니다.

테스트는 대상이 명확하다면 그 대상에만 집중해서 테스트를 하는 것이 바람직합니다.
따라서 테스트는 가능하면 작은 단위로 쪼개서 집중해야합니다.

관심사의 분리라는 원리가 테스트에도 적용 !

이렇게 작은 단위의 코드에 대해 테스트를 수행한 것을 단위 테스트(Unit test)라고 합니다.
단위 테스트를 하는 이유는 의도한 대로 동작하는지를 개발자 스스로 빨리 확인받기 위해서입니다.

2. 자동 수행 테스트 코드

테스트는 자동으로 수행되도록 코드로 만들어지는 것이 중요합니다.
자동으로 수행되면 테스트를 빠르게 실행할 수 있기 때문에 자주 반복해서 테스트할 수 있습니다.

3. 지속적 개선과 점진적 개발을 위한 테스트

테스트는 이용하면 새로운 기능이 기대한 대로 동작하는지 확인할 수 있습니다.
또한, 새로운 기능을 추가할 때 기존 코드에 영향을 받지 않고 잘 동작하는지를 확인할 수 있습니다.

4. 실행 결과 확인 자동화

테스트 결과를 개발자가 직접 확인하지 않아도 테스트의 성공, 실패에 대한 결과를 자동화해야합니다.

테스트 주도 개발 (TDD)

테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발을 테스트 주도 개발이라고 합니다.

TDD는 테스트를 먼저 만들고 그 테스트가 성공하도록 하는 코드를 만들기 때문에 테스트를 빼먹지 않고 꼼꼼하게 만들어낼 수 있습니다.
또, 매번 테스트가 성공하는 것을 보면서 작성한 코드에 확신을 가질 수 있습니다.

‘테스트를 만들고 자주 실행하면 개발이 지연되지 않을까?’ 라는 고민을 할 수도 있습니다.
하지만, 테스트 작성 시간이 짧기도하고 테스트 덕분에 오류를 빨리 잡아낼 수 있어서 전체적인 개발 속도는 오히려 빨라집니다.

JUnit

JUnit은 자바에서 단위 테스트를 지원하는 테스트 프레임워크입니다.

JUnit 테스트의 조건

JUnit 프레임워크로 테스트를 진행하기 위해서 2가지 조건을 따라야합니다.

  1. 메소드가 public으로 선언되어야합니다.
  2. 메소드에 @Test라는 애노테이션을 붙여야합니다.
Java
public class UserDaoTest {
  @Test
  public void andAndGet() throws SQLException {
    GenericXmlApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
  }
}

JUnit에는 다음처럼 assertThat() 스태틱 메서드로 값을 검증할 수 있습니다.

Java
assertThat(user.getName(), is("스프링"));

이런 메서드는 매처(matcher)라고 불리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어줍니다.

마지막으로 테스트는 항상 동일한 결과를 보장하도록 구현해야합니다.

JUnit의 부가 작업

1. @Before

@Before테스트를 실행 전 반복되는 로직을 처리하는 애노테이션입니다.

@Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메서드를 정의한 예제

Java
@Before
public void setup(){
  AplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
  this.dao = context.getBean("userDao", UserDao.class);
}

각 테스트 메소드에 반복적으로 나타났던 중복된 사전 작업을 제거하고 별도의 메서드로 작성할 수 있습니다.

2. @After

@After테스트 실행 이후 반복되는 로직을 처리하는 애노테이션입니다.

JUnit 동작 방식

JUnit이 하나의 테스트 클래스를 가져와 테스트를 수행하는 방식은 다음과 같습니다.

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

주의할 점

  1. @Before@After 메소드를 테스트 메소드에서 직접 호출하지 않기 때문에 서로 주고 받을 정보는 인스턴스 변수에 담아 사용해야합니다.
  2. 각 테스트 메소드를 실행할 때마다 테스트 클래스의 오브젝트를 새로 만듭니다.
    한번 만들어진 테스트 클래스의 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려집니다.
    덕분에, 인스턴스 변수도 부담 없이 사용 가능합니다.

    각 테스트가 서로 독립적으로 동작하도록 하기 위함 !

픽스처 (Fixture)

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

여러 테스트에서 반복적으로 사용되는 오브젝트는 다음과 같이 @Before 메소드를 이용해 생성해두면 편리합니다.

Java
public class UserDaoTest{
  private UserDao dao;
  private User user1;
  private User user2;

  @Before
  public void setUp(){
    this.user1 = new User("1", "A", "springno1");
    this.user2 = new User("2", "B", "springno2");
    // ...
  }
}

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

@Before 메소드가 테스트 메소드 개수만큼 반복되기 때문에 애플리케이션도 여러번 생성됩니다.

테스트는 가능한 독립적으로 매번 새로운 오브젝트를 생성해 사용하는 것이 원칙입니다.

하지만, 애플리케이션 컨텍스트처럼 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 합니다.

Java
// @ExtendWith(SpringExtension.class) // JUnit5 버전
// @SpringBootTest(classes = SpringStudy.class)
@RunWith(SpringJUnit4ClassRunner.class) // Junit4 버전
@ContextConfiguration(locations="/applicationContext.xml") // XML 버전
public class UserDaoTest {
  @Autowired
  private ApplicationContext context;
  // ...
}

@RunWith : JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션입니다.
@ContextConfiguration : 자동으로 만들어준 애플리케이션 컨텍스트의 설정파일 위치를 지정해줍니다.
@Autowired : 스프링의 DI에 사용되는 특별한 애노테이션입니다.

@Autowired 작동 원리

  1. @Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수타입과 일치하는 컨텍스트 내 빈을 찾습니다.
  2. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해줍니다.

일반적으로 주입을 위해 생성자 or 수정자 메소드를 사용하지만 이 경우에는 메소드가 없어도 주입이 가능

스프링 어플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록합니다.
따라서, 애플리케이션 컨텍스트에는 ApplicationContext 타입의 빈이 존재하는 셈이고 DI도 가능합니다.

만약 같은 타입이 2개 이상 있는 경우에는 타입만으로 어떤 빈을 가져올지 결정할 수 없어서 문제가 될 수 있습니다.

타입이 중복되면 변수도 확인, 변수도 중복되면 예외 발생 !

DI와 테스트

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

DAO가 테스트에서만 다른 DataSource를 사용하도록 할 수 있는 방법이 있을까?

이러한 경우 테스트 전용 설정파일을 따로 만들어두는 방법을 사용하면 됩니다.

예를 들어, 하나는 서버 운영용 DataSource, 다른 하나는 테스트용 DataSource를 작성해 테스트에서는 항상 테스트용 DataSource 설정 파일을 사용하도록 하면 됩니다.

컨테이너 없는 DI 테스트

다른 하나로는 아예 스프링 컨테이너를 사용하지 않고 테스트를 만드는 방식이 있습니다.

테스트 클래스 내에서 객체의 생성 및 관계 설정모두 직접 정의해줍니다.

이러한 방식의 장점으로는 애플리케이션 컨텍스트를 아예 사용하지 않으니 코드는 더 단순해지고 이해하기 편해집니다.
또한, 애플리케이션 컨텍스트가 만들어지는 번거로움이 없어져 테스트 시간도 절약할 수 있습니다.

반면에, 매번 새로운 테스트 오브젝트가 생성된다는 단점도 있습니다.