본문 바로가기
Back-End/토비의 스프링

토비의 스프링 | 2장. 테스트

by Havi 2017. 1. 13.
반응형

스프링의 가장 중요한 가치를 무엇이냐고 질문한다면 그 가치는 객체지향과 테스트에 있다. 확장과 변화를 고려한 객체지향적 설계와 그것을 효과적으로 담아낼 수 있는 IOC/DI 같은 기술이라면, 만들어진 코드에 확신을 주고 변화에 유연하게 대처할 수 있는 자신감을 주는 테스트 기술이 있다. 2장에서는 테스트란 무엇이며, 그 가치와 장점, 활용 전략, 스프링과의 관계를 살펴본다.

2.1 UserDaoTest 다시 보기

  • 이전에는 main() 메소드를 이용해 UserDao 오브젝트의 함수를 호출하고 결과물을 출력함으로써 테스트를 수행하였다.
  • 테스트란 결국 내가 예상했던 데로 코드가 정확히 동작하는지 확인해서, 내 코드에 대한 확신을 갖게 해주는 작업이다.
  • 테스트에서 원치 않는 결함이 나오면 이를 제거해가는 작업, 일명 디버깅을 거치게 되고, 이를 통해 결함이 없는 코드를 만들어 나가는게 테스트이다.

웹을 통한 DAO 테스트 방법의 문제점

보통의 웹 프로그래밍에서 DAO를 테스트하는 방법은 다음과 같다.

  • DAO를 만든 뒤 바로 테스트하지 않고, 서비스 계층, MVC 프레젠테이션 계층까지 포함한 모든 입출력 기능을 대충이라도 만든다.
  • 이렇게 만들어진 웹 애플리케이션을 서버에 배치한 뒤 웹 화면을 띄우고, 값을 직접 입력하며 테스트를 진행한다.
  • 이상이 없으면 입력한 값을 화면에 출력해 주는 기능을 만들어서 또 다른 기능까지 확인해 본다. 이런식의 테스트는 DAO, 서비스, 컨트롤러, 뷰 등 모든 레이어 기능을 다 만들고 나서야 테스트가 가능하다는게 가장 큰 문제이다. 테스트를 하는 중에 에러가 나거나 과연 어디에서 문제가 일어났는지에 대한 수고도 필요하다. 단순히 에러 메시지와 호출 스택 정보만 보고 간단히 원인을 찾을 수 없는 문제가 비일비재하다. 그렇다면 테스트를 어떻게 해야 효율적일수 있을까?

작은 단위의 테스트

테스트하고자 하는 대상이 명확하다면 그 대상에만 집중해서 테스트하는 것이 바람직하다. 테스트는 가능하면 작은 단위로 쪼개서 집중해서 할 수 있어야 한다.

  • UserDaoTest는 한 가지 관심에만 집중할 수 있게 작은 단위로 만들어진 테스트이다.
  • 다른 작업 필요없이 간단하게 IDE나 도스창에서 테스트 수행이 가능하다.
  • 엉뚱하게 JSP나 서블릿에서 에러가 발행해서 그것을 찾으려 시간낭비할 필요가 없다.
  • 이렇게 작은 단위로 테스트를 수행하는 것을 단위 테스트라 한다.
  • 여기서 말하는 단위는 크게 보자면 관리 기능을 모두 통틀어 하나의 단위로 볼 수도 있고, 작게 보자면 UserDao의 add()메소드 하나만을 단위라 생각할 수도 있다.
  • 처음부터 긴 테스트를 한번에 수행하면 디버깅하는데 시간이 오래 걸릴수도 있고, 기능은 잘 잘동하지만 원치 않는 값일수도 있는 여러 이슈가 있다.
  • 하지만, 작은 단위의 테스트를 수행하고 긴 테스트를 한다면 테스트 실패율이 줄어든고 디버깅 또한 훨씬 수월할 것이다.

UserDaoTest 문제점

  • 수동 확인 작업의 번거로움
    • UserDaoTest는 수행과정과 입력 데이터의 준비를 모두 자동으로 진행하도록 만들어졌다.
    • 모든 결과는 콘솔에 출력하는데, 결국 콘솔에 나온 결과를 확인하는 건 사람의 책임이다.
    • 테스트 수행은 자동으로 진행되지만 결과를 확인하는 책임은 완전히 자동으로 테스트한다 할 수 없다.
  • 실행 작업의 번거로움
    • 아무리 간단히 실행 가능한 main() 메소드라고 해도 DAO에 대한 작업이 확장되어 그것을 수백개 만들고 수백번 돌리는 것은 그만큼 수고스럽다.
    • 그래서 main()메소드보다 좀 더 편리하고 체계적으로 테스트하여 결과를 확인하는 방법이 필요하다.

2.2 UserDaoTest 개선

자동화 테스트를 위한 xUnit 프레임워크를 만든 켄트 백은 "테스트란 개발자가 마음 편하게 잠자리에 들 수 있게 해주는 것"이라고 했다. 개발을 완료했는데 기존 기능에 문제가 있지는 않을지, 새로운 기능을 추가했는데 회사에서 당장 문제를 해결하라 전화했을 때 등 우리는 다양한 문제에 직면하게 된다. 하지만 자동화된 코드로 테스트를 수행함으로써 이러한 문제들에 대해 조기에 대처할 수 있다. 우리가 수행한 단순한 main() 메소드로는 애플리케이션이 확장될 수록 테스트를 감당하는데에 한계가 있다. 이를 위해 좋은 테스팅 프레임워크인 JUnit이 존재한다.

  • 우리가 배웠던 제어의 역전(IOC)의 원리에 따라 프레임워크는 개발자가 만든 클래스에 대한 제어 권한을 넘겨받아 애플리케이션의 흐름을 제어한다.
  • 따라서 기존에 만들었던 main()은 제어권을 직접 갖고 있다. 그래서 우리는 테스트 코드를 일반 메소드로 옮겨야 한다.
  • JUnit이 요구하는 조건은 다음 두 가지가 있다.
    • 메소드를 public으로 선언해야 한다.
    • @Test라는 애노테이션을 붙여야 한다.
import org.junit.Test;
...

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicaionContext.xml");
        UserDao dao = context.getBean("userDao", UserDao.class);
        ...
    }
}
  • main 메소드를 대신하여 일반 메소드에 적절한 이름을 붙여주며 테스트의 의도를 나타내는 이름이 좋다.
  • 접근지정자를 public으로 주는것을 잊으면 안된다.

검증 코드 전환

자바코드로 검증을 구하는 조건을 표현하면 다음과 같다.

//user 오브젝트의 name 값과 user2 오브젝트의 name 값이 같으면 다음으로 넘어가고 아니면 테스트 실패
if(!user.getName().equals(user2.getName())) {...}

위의 문장을 JUnit에서 제공해주는 assertThat이라는 스태틱 메소드를 이용하면 다음과 같다.

assertThat(user2.getName(), is(user.getName()));
  • assertThat() 메소드는 첫 번째 파라미터의 값을 뒤에 나오는 매치(matcher)라고 블리는 조건으로 비교해서 일치하면 다음으로 넘어가고, 아니면 테스트 실패를 뱉어낸다.
  • is()는 매처의 일종으로 equals()로 비교해주는 기능을 가졌다.
  • JUnit은 다양한 방법으로 테스트 결과를 알려주므로 굳이 수동으로 "테스트 성공"이라는 메시지를 출력할 필요가 없다. 최종코드는 다음과 같다.
import org.junit.Test;
import static org.junit.Assert.assertThat;
...

public class UserDaoTest {
    @Test
    public void addAndGet() throws SQLException {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicaionContext.xml");

        UserDao dao = context.getBean("userDao", UserDao.class);
        User user = new User();
        user.setId("kim");
        user.setName("김영재");
        user.setPassword("spring");
        dao.add(user);

        User user2 = dao.get(user.getId());

        assertThat(user2.getName(), is(user.getName()));
        assertThat(user2.getPassword(), is(user.getPassword()));
    }
}

JUnit 테스트 실행

간단하게 클래스안에 main() 메소드를 하나 추가하고, @Test 테스트 메소드를 가진 클래스의 이름을 넣어준다.

import org.junit.runner.JUnitCore;
...
public static void main(String[] args) {
    JUnitCore.main("spring.user.dao.UserDaoTest");
}

2.3 개발자를 위한 테스팅 프레임워크 JUnit

가장 좋은 JUnit 실행방법은 IDE에 내장된 JUnit 테스트 지원도구를 사용하는 것이다. @Test가 들어 있는 테스트 클래스 선택 후 RUN AS 항목 중에서 JUnit Test를 선택하면 테스트가 자동으로 실행된다. JUnitCore를 사용할 때처럼 main() 메소드를 만들지 않아도 된다. 최종결과는 테스트의 총 수행시간, 실행한 테스트의 수, 테스트 에러의 수, 테스트 실패의 수를 확인할 수 있다. 또한, 실패한 이유는 뷰의 아래 Failure Trace 항목에 자세히 나와있다.

addAndGet() 테스트 보완

add()에 대한 검증과 get()에 대한 검증 모두 보완할 필요가 있다. 또한, id를 조건으로 해서 사용자를 검색하는 기능을 가진 get()에 대한 테스트도 필요하다. 아래와 같이 코드를 작성함으로써 get()메소드에 대한 검증여부에 대해 좀 더 확신할 수 있게 됐다.

@Test
public void addAndGet() throws SQLException {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicaionContext.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("young1", "김영재", "spring1");
    User user1 = new User("young2", "김영철", "spring2");

    //모든 데이터를 삭제하고 이를 검증하기 위해 추가되었다.
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));

    //DB에 데이터가 add되는것에 대한 검증부분이다.
    dao.add(user1);
    dao.add(user2);
    assertThat(dao.getCount(), is(2));

    //user1 오브젝트와 get()으로 가져온 userget1을 검증한다.
    User userget1 = dao.get(user1.getId());
    assertThat(userget1.getName(), is(user1.getName()));
    assertThat(userget1.getPassword(), is(user1.getPassword()));

    //user2 오브젝트와 get()으로 가져온 userget2을 검증한다.
    User userget2 = dao.get(user2.getId());
    assertThat(userget2.getName(), is(user2.getName()));
    assertThat(userget2.getPassword(), is(user2.getPassword()));
}

get() 예외조건에 대한 테스트

get() 메소드에 전달된 id값에 해당하는 정보가 없으면 어떻게 할까? 하나는 null과 같은 특별한 값을 리턴하는 것이고, 다른 하나는 정보를 찾을 수 없다는 예외를 던지는 것이다. 각각의 장단점이 있지만 후자를 택해서 진행해 보자. 스프링이 정의 한 다양한 데이터 액세스 예외 클래스중 EmptyResultDataAccessException 예외를 이용하자.

@Test(exptected=EmptyResultDataAccessException.class) //테스트중 발생할 것으로 기대되는 예외 클래스 지정
public void getUserFailure() throws SQLException {
    ApplicationContext context = new ClassPathXmlApplicationContext("applicaionContext.xml");

    UserDao dao = context.getBean("userDao", UserDao.class);
    User user1 = new User("young1", "김영재", "spring1");
    User user1 = new User("young2", "김영철", "spring2");

    //모든 데이터를 삭제하고 이를 검증하기 위해 추가되었다.
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));
}

위의 테스트코드를 실행하면 테스트는 실패할 것이다. get() 메소드로 로우값을 가져와야 하지만 해당 값을 가져오는 코드가 없기 떄문이다. 아래와 같이 추가적인 코드를 작성하자.

public User get(String id) throws SQLException {
    ...
    ResultSet rs = ps.executeQuery();

    User user = null; //user를 null로 초기화
    if(rs.next()) {
        user = new User();
        user.setId(rs.getString("id"));
        user.setName(rs.getString("name"));
        user.setPassword(rs.getString("password"));
    }

    rs.close();
    ps.close();
    c.close();

    if(user == null) { throw new EmptyResultDataAccessException(1); }

    return user;
}
  • 위의 코드는 이전과는 반대로 expected에서 지정한 예외가 던져지면 성공이고, 정상적으로 테스트 메소드를 마치면 실패라고 뜬다.
  • 예외가 반드시 발생해야 하는 경우에 유용하다.

포괄적인 테스트

  • 개발자들은 일반적으로 잘 돌아가게끔 성공적인 테스트를 만드는 경향이 있다.
  • 그렇기 떄문에 QA팀이 준비된 시나리오에 따라 꼼꼼히 테스트 해주지만 네거티브 테스트를 만드는 습관을 기를 필요가 있다.
  • 다양한 예외적인 상황을 꼼꼼히 체크하는 것이 개발자의 덕목중 하나이다.

테스트가 이끄는 개발

작업한 순서를 잘 살펴보면 테스트를 먼저 만들어 테스트가 실패한 것을 보고 나서 UserDao에 손을 대기 시작했다. 실제로 이러한 개발 전략이 존재한다.

  • 우리는 '존재하지 않는 id로 get() 메소드를 실행하면 특정한 예외가 던져져야 한다' 식으로 만들어야 할 기능을 결정했다.
  • 그것은 만들어진 코드를 보고 이것을 어떻게 테스트할까라는 식으로 getUserFailure()를 만든 것이 아니라, 추가하고 싶은 기능을 코드로 표현하려고 했기 때문에 가능했다.
  • getUserFailure() 테스트에는 만들고 싶은 기능에 대한 조건과 행위, 결과에 대한 내용이 잘 표현되어 있다.
단계내용코드
조건어떤 조건을 가지고가져올 사용자 정보가 존재하지 않는 경우에dao.deleteAll();
assertThat(dao.getCount(), is(0));
행위무엇을 할 때존재하지 않는 id로 get()을 실행하면get("unknown_id");
결과어떤 결과가 나온다특별한 예외가 던져진다@Test(expected=EmptyResultDataAccessException.class)
  • 얼핏보면 기능설계, 구현, 테스트라는 일반적인 개발 흐름의 기능설계를 담당한다고 볼 수 있다.
  • 이런 식으로 추가하고 싶은 기능을 테스트 코드로 표현해서, 실제 기능을 가진 애플리케이션 코드를 만들고, 테스트를 실행해서 설계한 대로 잘 동작하는지 검증 할 수 있다.
  • 그럼으로써 코드 구현과 테스트라는 두 가지 작업이 동시에 끝나는 것이다.

테스트 주도 개발

만들고자 하는 기능의 내용을 담고 테스트 코드를 먼저 만들고, 테스트를 성공하게 해주는 코드를 작성하는 방식의 개발을 테스트 주도 개발이라 한다.

  • 대부분의 개발자들은 빨리 기능을 완성하고 싶은 욕구와 한번에 빠져드는 집중력때문에 테스트 코드작성을 소홀히 하게 된다. 나중에는 작성한 코드가 많아 무엇을 테스트해야 할지 막막할 수도 있다. 결국 테스트는 자꾸 뒷전으로 밀려나게 될 것이다.
  • TDD는 아예 테스트 코드를 먼저 만들고 그 테스트가 성공하도록 하는 코드만 만드는 식으로 진행하기 때문에 테스트를 빼먹지 않고 꼼꼼히 만들 수 있다. 또한 테스트 작성과 수행할 때까지 걸리는 시간은 매우 짧다.
  • 테스트 작성 작업의 주기는 짧게 가져가는 것이 좋다.
  • 개발자는 이미 기본적인 테스트를 머리속에서 수행한다. 보통 개발할 때나 설계할 때, 전체 프로세스를 시뮬레이션으로 머리속에 그리는데 그러한 것들을 끄집어 내서 작성하는 것이 TDD이다.
  • 머리속에서 시뮬레이션하는것 보다 직접 테스트 코드를 짜고 진행한다면 더 확신있고 삽질없이 개발을 진행할 수 있을 것이다. 그러므로 TDD를 시작하자!

테스트 코드 개선

테스트시 중복되는 코드를 리펙토링 해보자. 다음의 코드는 Bean 오브젝트를 가져올때 항상 수행이 된다.

ApplicationContext context = new GenericXmlApplicationContext("applicationContext.xml");
User dao = context.getBean("userDao", UserDao.class);

@Before 이를 매번 테스트 메소드를 실행하기 전에 먼저 실행시켜주는 기능이다. 이를 구현시키면 다음과 같다.

import org.junit.Test;
...

public class UserDaoTest {
    private UserDao dao; //setUP()이외의 다른 메소드에서도 접근 가능하도록 인스턴스 변수로 선언

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

    @Test
    public void addAndGet() throws SQLException {
        ...
    }

    @Test
    public void count() throws SQLException {
        ...
    }

    @Test(exptected=EmptyResultDataAccessException.class)
    public void getUserFailure() throws SQLException {
        ...
    }
}
  • 이러한 테스트 메소는 매 실행마다 새로운 오브젝트를 만든다. 즉, 한번 만들어진 오브젝트는 하나의 테스트 메소드를 사용하고 나면 버려진다.(ex.addAndGet(), count(), getUserFailure() 모두 각각 따로 만들어 진다.)
  • 왜나하면, 각 테스트가 서로 영향을 주지 않고 독립적으로 실행됨을 보장해 주기 위해서 이다.
  • 공통적으로 사용하기 위해서는 @Before보다는 일반 메소드를 사용하여 직접 호출해 사용하도록 하는게 좋다.

픽스처

  • 테스트를 수행하는 데 필요한 정보나 오브젝트
  • UserDaoTest에서는 dao가, add() 메소드에 전달하는 User 오브젝트가 대표적인 픽스처이다.
  • 이러한 오브젝트들은 @Before에서 생성하도록 묶어놓는 것이 좋다.

2.4 스프링 테스트 적용

한 가지 찜찜한 점이 어플리케이션 컨텍스트가 매번 새로 만들어 진다는 점이다. 처음 딱 한 번만 생성해 놓으며 재사용하는 것이 훨씬 효율이 좋을 것이다. JUnit은 이를 위해 테스트 클래스 전체에 걸쳐 딱 한 번만 실행되는 @BeforeClass 스태틱 메소드를 지원한다. 하지만 이보다는 스프링이 직접 제공하는 애플리케이션 컨텍스트 테스트 지원 기능을 사용하는 것이 더 편리하다.

@RunWith(SpringJUnit4ClassRunner.class) //스프링의 테스트 컨텍스트 프레임워크의 JUnit 확정기능
@ContextConfiguration(locations="/applicationContext.xml") //테스트 컨텍스트가 자동으로 만들어줄 애플리케이션 컨텍스트의 위치 지정
public class UserDaoTest {
    @Autowired
    private ApplicationContext context; //테스트 오브젝트가 만들어지고 나면 스프링 테스트 컨텍스트에 의해 자동 주입
    ...

    @Befire
    public void setUo() throws SQLException {
        this.dao = this.context.getBean("userDao",  UserDao.class);
        ...
    }
}
  • @RuntWith는 JUnit 프레임워크의 테스트 실행 방법을 확장할 때 사용하는 애노테이션이다.
  • @ContextConfiguration은 자동으로 만들어줄 애플리케이션 컨텍스트의 설정파일 위치 설정이다.
  • 실제로 context는 초기에 한 번만 생성되서 사용되므로 테스트 메소드는 갈수록 속도가 향상된다.

테스트 클래스의 컨텍스트 공유

스프링 테스트 컨텍스트 프레임워크는 다음과 같이 여러 개의 테스트 클래스가 존재해도 하나의 애플리케이션 컨텍스트를 공유한다. 즉, 수천개의 테스트 클래스가 있어도 하나의 애플리케이션 컨텍스트만을 사용한다.

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

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

@Autowired

  • 스프링의 DI에 사용되는 특별한 애노테이션이다.
  • @Autowired가 붙은 인스턴스 변수가 있으면, 테스트 컨텍스트 프레임워크는 변수 타입과 일치하는 컨텍스트 내의 빈을 찾는다. 타입이 일치하는 빈이 있으면 인스턴스 변수에 주입해준다.
  • 일반적으로 주입에는 생성자나 수정자 메소드가 필요하지만, 이 경우에는 따로 필요가 없다.
  • UserDao 빈도 직접 DI 받을 수 있다. ApplicationContext 인스턴스 변수를 삭제하고 수정해 보자.
...
public class UserDaoTest {
    @Autowired
    UserDao dao; //DI로 받아서 DL방식보다 훨씬 깔끔하다.
}
  • 단, @Autowired는 같은 타입의 빈이 두 개 이상 있는 경우에는 어떤 빈을 가져올지 결정할 수 없다. DataSource 타입의 빈이 두 개 있는데, 하나는 dataSource이고 하나는 dataSource2라면 첫 번째 빈이 주입될 것이다.
  • 변수 이름으로도 찾을 수 없으면 예외가 발생한다.
  • SimpleDriverDataSource나 DataSource 타입중 어떤 것을 DI받고 싶은지 고민된다면 단순히 테스트의 역할에 맞춰 선택하면 된다.
    • DB 연결정보를 직접 확인하고 싶다면 SimpleDriverDataSource
    • DataSource로 선언한다면 DataSource 설정이 변경되더라도 따로 테스트 코드를 건드릴 필요가 없다.
  • 가능한 인터페이스를 사용해서 애플리케이션 코드와 결합도를 낮추는 것이 좋다.

DI와 테스트

우리는 UserDao와 DB 커넥션 생성 클래스 사이에 DataSource라는 인터페이스를 뒀다. 때문에 코드의 수정 없이도 의존 오브젝트를 바꿔가며 사용할 수 있고, DI를 통해 오브젝트 주입받으므로 오브젝트 생성에 부담이 없다. 그런데 이러한 반문이 있을 수 있다. 굳이 DataSource 인터페이스를 사용하고 DI를 주입받아야 할까? 그냥 UserDao에 직접 SimpleDriverDataSource를 사용하면 안될까? 아니다 그래도 인터페이스를 두고 DI를 적용해야 한다.

  • 첫째, 소프트웨어 개발에서 절대로 바뀌지 않는 것은 없기 때문이다.
    • 간단하게 DI를 통해 주입받으면 new로 생성하는 것보다 수정하는데 비용과 시간이 절감된다.
  • 둘째, 인터페이스를 두고 DI를 적용하면 다른 차원의 서비스 기능을 도입할 수 있기 때문이다.
    • 1장에서 만든 DB 커넥션의 개수를 카운팅하는 부가기능이 그 예이다.
    • 기존코드에는 전혀 손댈 필요없고 추가했던 기능도 언제든지 간단히 수정 or 삭제할 수 있다. 스프링은 이런 기법을 일반화해서 AOP라는 기술로 만들어주기도 한다.
  • 셋째, 테스트 때문이다.
    • 빠르게 동작하는 테스트 코드를 위해서는 가능한 한 작은 단위의 대상에 대한 테스트를 수행해야 한다.

DI를 테스트에 이용하는 방법 3가지

1)테스트 코드에 의한 DI

  • 테스트 코드에서 직접 DataSource를 직접 생성하여 DI해도 된다.
  • SingleConnectionDataSourc을 사용하여 DB 커넥션을 하나만 만들어두고 계속 사용하기에 빠르다.
  • @DirtiesContext는 스프링의 테스트 컨텍스트 프레임워크에게 해당 클래스의 테스트에서 애플리케이션 컨텍스트의 상태변경을 알려준다.
  • 즉, 매번 새로운 애플리케이션 컨텍스트를 만들어 사용한다.

    ...
    @DirtiesContext
    public class UserDaoTest {
    @Autowired
    UserDao dao;
    ...
    
    @Befire
    public void setUo() throws SQLException {
        ...
        DataSource dataSource = new SingleConnectionDataSourc("jdbc:mysql://localhost/testdb", "srping", "book", true);
        dao.setDataSource(dataSource); //코드에 의한 수동 DI
    }
    }

2)테스트를 위한 별도의 DI설정 빈 오브젝트에 수동으로 DI하는 방법은 잠정보다 단점이 많다. 때문에 기존의 applicaionContext.xml을 복사해서 test-applicationContext.xml을 만들어 테스트때만 사용하는 방법이 있다.

3)컨테이너 없는 DI 테스트 원한다면 스프링 컨테이너를 이용해서 IOC 방식으로 생성되고 DI 되도록 하는 대신, 테스트 코드에서 직접 오브젝트를 만들고 DI 해서 사용해도 된다. 매번 새로운 테스트 오브젝트를 만드는 번거로움이 있지만 애플리케이션 컨텍스트를 만드는 부담이 없어 코드가 더 단순해지고 깔끔해 졌다.(+시간절약)

public class UserDaoTest {
    UserDao dao;
    ...

    @Befire
    public void setUo() {
        ...
        dao = new UserDao();
        DataSource dataSource = new SingleConnectionDataSourc("jdbc:mysql://localhost/testdb", "srping", "book", true);
        dao.setDataSource(dataSource); //오브젝트 생성, 관계설정 등을 모두 직접 해준다.
    }
}

비침투적 기술: 애플리케이션 로직을 담은 코드에 아무런 영향을 주지 않고 적용이 가능하다. 따라서 기술에 종속적이지 않은 순수한 코드를 유지하게끔 해준다. 스프링은 이런 비침추적 기술의 대표적인 예이며 그래서 스프링 컨테이너 없는 DI 테스트도 가능하다.

DI를 이용한 테스트 방법 선택

  • 일반적으로 테스트하기 좋은 코드가 좋은 코드일 가능성이 높다.
  • 위의 3가지 방법 중 각각의 장단점을 고려하여 선택해야 한다.
  • 항상 스프링 컨테이너 없이 테스트할 수 있는 방법을 가장 우선적으로 고려하자. 이 방법이 테스트 수행 속도가 가장 빠르고 테스트 자체가 간결하다.
  • 복잡한 의존관계를 갖고 있을 경우 따로 설정파일을 두는 테스트를 고려하자.
  • 테스트 설정이 따로 있다 하더라도 때로는 예외적인 의존관계를 강제로 구성해서 테스트해야 할 경우가 있다. 이때는 수동 DI 해서 테스트하는 방법을 사용하면 된다.(@DirtiesContext를 클래스나 메소드에 붙이는 것을 잊지말자.)

2.5 학습 테스트로 배우는 스프링

자신이 짠 코드가 아닌 남들이 짠 코드에 대해 제대로 알기위해 테스트 코드를 작성해 보는것을 학습 테스트라 한다. 학습 테스트의 목적은 검증과 테스트 대상이 목적이 아니라 테스트 코드 자체에 관심을 가져야 한다. 장점은 다음과 같다.

  • 다양한 조건에 따른 기능을 손쉽게 확인해볼 수 있다.
  • 학습 테스트 코드를 개발 중에 참고할 수 있다.
  • 프레임워크나 제품을 업그레이드할 때 호환성 검증을 도와준다.
    • 요즘에는 오픈소스 프레임워크가 자주 업데이트 된다. 때문에 기존에는 잘 작동되던 것들도 문제가 생길 경우가 있다. 테스트 코드를 사용하면 이러한 부담이 상대적으로 줄어든다.
  • 테스트 작성에 대한 좋은 훈련이 된다.
  • 새로운 기술을 공부하는 과정이 즐거워진다.

스프링 테스트 컨텍스트 테스트

스프링의 테스트용 애플리케이션 컨텍스트는 테스트 개수에 상관없이 한 개만 만들어진다. 그리고 모든 테스트에 공유된다. 정말 그런지 검증해 보자.

...

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/applicationContext.xml")
public class UserDaoTest {
    @Autowired
    private ApplicationContext context;
    static Set<JUnitTest> testObjects = new HashSet<JUnitTest>();
    static ApplicaionContext contextObject = null;

    @Test
    public void test1() {
        assertThat(testObjects, not(hashItme(this)));
        testObjects.add(this);
        assertThat(contextObject == null || contextObject == this.context, is(true));
        contextObject = this.context;
    }

    @Test
    public void test2() {
        assertThat(testObjects, not(hashItme(this)));
        testObjects.add(this);
        assertTrue(contextObject == null || contextObject == this.context);
        contextObject = this.context;
    }

    @Test
    public void test3() {
        assertThat(testObjects, not(hashItme(this)));
        testObjects.add(this);
        assertThat(contextObject, either(is(nullValue())).or(is(this.context)));
        contextObject = this.context;
    }
}
  • test1은 contextObject가 null이면 처음에 만들어진 것으로 간주하고 this.context이면 기존에 있는 것을 재활용한다는 의미로 간주하며 is는 true와 비교하면 된다.
  • test2는 조금더 간결하게 assertTrue를 사용하여 비교한다.
  • test3는 either()가 뒤에 이어서 나오는 or()와 함께 두 개의 매처의 결과를 OR 조건으로 비교해 준다. 두 가지 매처 중 하나만 true가 나와도 성공이다. nullValue()는 null인지를 확인해 준다.

동등분할: 같은 결과를 내는 값의 범위를 구분해서 각 대표 값으로 테스트를 하는 방법을 말한다. 결과값이 true, false 또는 예외상황 등 여러 경우가 있다면 모든 경우에 대한 테스트를 해보는게 좋다.

경계값 분석: 에러는 동등분할 범위의 경계에서 주로 많이 발생한다는 특징을 이용해서 경계의 근처에 있는 값을 이용해 테스트하는 방법이다. 보통 숫자의 경우 0이나 그 주변 값 또는 정수의 최대값, 최소값 등으로 테스트한다.


반응형

댓글