Today I Learned_230506

5 분 소요

토비의 스프링 3.1

스프링 테스트 적용

@Before는 테스트 메소드 개수만큼 반복 → 어플리케이션 컨텍스트도 세 번 만들어진다.

어플리케이션 컨텍스트가 만들어질 때는 모든 싱글톤 빈 오브젝트를 초기화

→ 이와 같이 생성에 많은 시간과 자원이 소모되는 경우에는 테스트 전체가 공유하는 오브젝트를 만들기도 한다.

빈은 싱글톤으로 만들어져있기에 상태를 갖지 않음

→ 어플리케이션 컨텍스트는 한 번만 만들고 여러 테스트가 공유해서 사용해도 된다.

@BeforeClass

  • 테스트 클래스 전체에 걸쳐 딱 한번만 실행되는 스태틱 메소드

→ 스프링이 제공하는 어플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 편리함

간단한 어노테이션 설정만으로 테스트에서 필요로 하는 애플리케이션 컨텍스트를 만들어서 모든 테스트가 공유하게 할 수 있다.

JUnit 테스트 컨텍스트 프레임워크를 사용하면 어노테이션 설정만으로 모든 테스트가 어플리케이션 컨텍스트를 공유하도록 할 수 있다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        JUnitCore.main("com.example.demo3.dao.UserDaoTest");
    }
    @Autowired
    private ApplicationContext context;

    @Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
    public void setUp() {
        this.dao = context.getBean("userDao", UserDao.class);
  • RunWith : JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션
    • SpringJUnit4ClassRunner를 지정 → 테스트를 진행하는 중에 테스트가 사용할 어플리케이션 컨텍스트를 만들고 관리하는 작업을 진행해 준다.
  • @ContextConfiguration : 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치 지정
  • JUnit 확장기능은 테스트가 실행되기 전에 딱 한 번만 애플리케이션 컨텍스트를 만든다
    • 테스트 오브젝트가 만들어질 때마다 어플리케이션 컨텍스트 자신을 테스트 오브젝트의 특정 필드에 주입한다 (DI)

→ 하나의 테스트 클래스 내의 테스트 메소드는 같은 애플리케이션 컨텍스트를 공유해서 사용한다.

  • 같은 설정 파일(appilcationContext.xml)을 사용한다면 테스트 수행 중에 단 하나의 어플리케이션 컨텍스트만 만들어진다.
  • 다른 테스트 클래스 사이에서도 같은 어플리케이션 컨텍스트를 공유한다.

@Autowired

  • 스프링의 DI에 사용되는 어노테이션. 자동 와이어링을 진행해 준다.
  • @Autowired가 붙은 인스턴스 변수가 있으면, 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다.
  • 같은 타입의 빈이 두 개 이상 있는 경우, 변수의 이름과 같은 이름의 빈이 있는지 확인
  • 변수 이름으로도 빈을 찾을 수 없는 경우 예외 발생

스프링 애플리케이션 컨텍스트는 초기화할 때 자기 자신도 빈으로 등록

→ 애플리케이션에는 ApplicationContext라는 타입의 빈이 존재하고, DI도 가능하다.

테스트는 필요하다면 얼마든지 어플리케이션 클래스와 밀접한 관계를 맺고 있어도 된다.

→ 꼭 필요하지 않다면 가능한 한 인터페이스를 사용해서 애플리케이션 코드와 느슨하게 연결해 두는 편이 좋다.

개발하는데 DI를 적용해야 하는 이유

  1. 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없다.
    1. 당장 바꿀 계획이 없어도, 언제 어떻게 바뀔지 모른다.
  2. 클래스의 구현 방식이 바뀌지 않더라도, DI를 적용하면 다른 차원의 서비스 기능을 도입하기 쉬워진다.
    1. 예를 들면 DB 커넥션을 카운팅하는 기능.. DI가 들어간다면 구현하기 쉬워진다.
  3. 효율적인 테스트를 만들기 위함
    1. DI는 테스트가 작은 단위의 대상에 대해 독립적으로 만들어지고 실행되게 하는데 중요한 역할을 한다.

테스트 코드에 의한 DI

DI를 사용하여 테스트 중에 의존 관계를 바꿀 수 있다.

운영과 테스트의 환경이 다름 → 테스트 DI를 통해 해결할 수 있다.

예제

public void setUp() {
        DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "spring", "book", true);

장점

  • XML 설정파일을 수정하지 않고도 테스트 코드를 통해 오브젝트 관계를 재구성 할 수 있음

단점

  • 스프링 애플리케이션 컨텍스트는 싱글톤 빈이므로 테스트 중에 딱 한개만 만들어진다.
    • 싱글톤이므로 상태 변경을 하지 않는 것이 원칙
  • 의존관계를 강제로 변경 → 한번 변경하게 되면 나머지 테스트가 변경한 내용을 그대로 사용할 것
  • 이렇게 수동으로 DI하는 것은 장점보다 단점이 많다.

해결 방법 → @DirtiesContext 사용

  • 스프링 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태가 변경됨을 알려줌
  • 이 어노테이션이 붙은 테스트 클래스에는 애플리케이션 컨텍스트 공유를 허용하지 않음
  • 테스트 메소드 수행 후, 매번 새로운 애플리케이션 컨텍스트 생성
    • 변경한 컨텍스트가 다음 테스트에 영향을 주지 않기 위함
  • 메소드 레벨에도 적용 가능

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

  • 테스트 전용 어플리케이션 컨텍스트 설정 파일(applicationContext.xml)을 따로 만드는 것.

컨테이너 없는 DI 테스트

  • 스프링 컨테이너를 사용하지 않고 테스트를 만드는 것
  • UserDaoTest는 스프링 컨테이너가 제대로 동작하는지 관심을 가질 필요가 없다.
  • 직접 객체를 생성하고 DI를 진행한다.

예시

//    @Autowired 제거
    private UserDao dao;

    @Before // @Test 메소드가 실행되기 전에 먼저 실행되어야 하는 메소드 정의
    public void setUp() {
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSource("jdbc:mysql://localhost/testdb", "root", "", true);
        dao.setDataSource(dataSource);
  • 코드가 단순해지고 이해하기 편해짐
  • 매번 UserDao 오브젝트가 새로 만들어지긴 하지만, 가벼운 오브젝트이므로 부담이 되지 않는다.

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

침투적 기술 (Invasive)

  • 기술을 적용했을 때 애플리케이션 코드에 기술 관련 API가 등장
  • 특정 인터페이스나 클래스를 사용하도록 강제
  • 애플리케이션 코드가 해당 기술에 종속된다.

비침투적 기술 (noninvasive)

  • 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다.
  • 기술에 종속적이지 않은 순수한 코드의 유지가 가능
  • 스프링은 비침투적인 기술의 대표적인 예

DI를 이용한 테스트 방법

  1. 스프링 컨테이너 없이 테스트 할 수 있는 방법
    1. 객체를 직접 만들고, DI도 직접 하자
    2. 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
  2. 스프링의 설정을 이용한 DI방식의 테스트
    1. 여러모로 복잡한 의존관계를 가지고 있을 때 사용
    2. 테스트 전용 설정 파일을 따로 만든다.
  3. 수동 DI 테스트
    1. 예외적인 의존관계를 강제로 구성해야 할 경우
    2. @DirtiesContext 어노테이션 필요

학습 테스트

자신이 만들지 않은 프레임워크나 다른 개발팀에서 만들어서 제공한 라이브러리에 대한 테스트

학습 테스트의 목적

  • 자신이 사용할 API나 프레임워크의 기능을 테스트로 보면서 사용 방법을 익히는 것
  • 기능 검증이 목적은 아니다.
  • 기술이나 기능에 대해 얼마나 이해하고 있는지, 사용 방법을 제대로 알고 있는지 확인하는 것이 목적

학습 테스트의 장점

  • 다양한 조건에 따른 기능을 손쉽게 확인해 볼 수 있다.
    • 자동화된 테스트로 만들어지므로 손쉽게 확인 가능
  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
    • 실제 개발 코드에서 샘플로 참고 가능
  • 프레임워크나 제품을 업그레이드 할 때 호환성 검증을 도와준다
    • 기존에 사용하던 API 기능에 변화가 있거나 업데이트 되었는지 확인 가능
  • 테스트 작성에 대한 좋은 훈련이 된다
    • 실제로 프레임워크를 사용하는 애플리케이션 코드의 테스트와 유사하므로 테스트 작성에 대한 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다

스프링 자신에 대한 테스트 코드 → 스프링 학습 테스트를 만들 때 참고할 수 있는 가장 좋은 소스

학습 테스트 예제 - JUnit 테스트 오브젝트 테스트

JUnit은 테스트 메소드를 수행할 때 마다 새로운 오브젝트를 만드는지 확인

JUnit으로 만드는 JUnit 자신에 대한 테스트

package com.example.demo3.learningtest.junit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class JUnitTest {
    static JUnitTest testObject;

    @Test
    public void test1() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }

    @Test
    public void test2() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }

    @Test
    public void test3() {
        assertThat(this, is(not(sameInstance(testObject))));
        testObject = this;
    }
}
  • not() : 뒤에 나오는 결과를 부정하는 매처
  • sameInstance() : 실제로 같은 오브젝트인지 비교 → 동일성 비교 매처
  • 이 테스트의 단점 → 바로 직전의 오브젝트만 비교

개선

static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();

    @Test
    public void test1() {
        assertThat(testObjects, not(hasItem(this)));
        testObjects.add(this);
    }
  • hasItem() : 컬렉션의 원소인지를 검사하는 매처
assertThat(contextObject == null || contextObject == this.context, is(true));
 
assertTrue(contextObject == null || contextObject == this.context);

assertThat(contextObject, either(is(nullValue())).or(is(this.context)));
  • is()는 타입만 맞으면 어떤 값이든 검증할 수 있다.
  • assertTrue() : 검증용 메소드
  • either() : 뒤에 이어서 나오는 or()와 함께 두 개의 매처의 결과를 or조건으로 비교
  • nullValue() : 오브젝트가 null인지 확인

버그 테스트

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

일단 실패하도록 만들어야 한다. 버그가 원인이 되어서 테스트가 실패하는 코드를 만들자.

장점

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

동등 분할 : 같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법

경계값 분석 : 에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해 경계값 근처에 있는 값을 이용해 테스트

카테고리:

업데이트: