테스트와 JUnit

Spring 2016. 6. 27. 16:43


스프링이 개발자에게 제공하는 중요한 가치라고 한다면 객체지향과 테스트를 꼽을 수 있습니다.

대부분 테스트 라는 것을 조금 멀리하는 경향이 있는데.. 이는 사실 잘못된 것입니다.

소프트웨어 개발은 건물을 짓는 것과 같습니다.

한층 한층 지어갈 때 마다 지어진 곳에 문제는 없는지, 부실하진 않은지 등을 항상 검사해야 합니다.

이를 하지 않으면 당장 빨리 건물을 지어 내보일 순 있겠지만.. 폭탄과 같습니다.

언제 무너질지 모르기 때문이죠..


소프트웨어 개발도 마찬가지 입니다. 

테스트를 거치면서 견고하게 개발을 하지 않으면 언제 소프트웨어에 장애가 발생할 지 모릅니다.

개발한 부분에 대해서는 어떠한 방법으로라도 꼭 테스트를 해야 합니다.

코드를 보고 머릿속으로 시뮬레이션을 하거나, 돌아가겠지~ 하고 무책임하게 개발을 해서는 절대 안됩니다.



단위테스트

이러한 테스트를 간편하게 해주는 JUnit에 대해 설명하기 전에, 먼저 단위 테스트 라는 단어에 대해 알아보겠습니다.

단위 테스트란 말 그대로 테스트를 어떠한 단위로 나누어 테스트 하는 것 입니다.

한번에 너무 많은 것을 몰아서 테스트하면, 수행 결과도 복잡해지고 오류 발생 시 원인을 찾기도 힘들어집니다.


예를 들어 회원가입 시 비밀번호에 문자,특수문자,숫자 가 들어갔는지를 검증하는 로직을 테스트하려고

회원 가입 페이지를 띄워서 테스트를 하는 것은 바람직하지 않죠..

톰캣 구동, 디비 연결, 패스워드 검증 외 로직들 까지 한번에 테스트하는 꼴이 되어버립니다.

이러한 경우 오류가 발생했을 때 어디서 발생했는지 알기도 쉽지 않습니다. 정말 패스워드 검증 로직에서

오류가 발생했을 수도 있지만 DB 연결에서 오류가 발생했을 수도 있고, jsp 뷰에서 오류가 발생했을 수도 있죠.


그래서 테스트는 가능하면 작은 단위로 쪼개서 테스트하고자 하는 대상에 집중해서 할 수 있게 해야 합니다.

딱히 정해진 단위(클래스 단위, 메서드 단위, 로직 단위 등)는 없지만, 일반적으로는 작은게 좋습니다.

그리고 이러한 단위테스트 말고도 언젠간 이런 단위테스트를 다 합친 통합 테스트도 필요합니다.



보통 단위테스트 용으로 main 메서드를 자주 사용하지만, 

매번 실행때마다 테스트 결과를 눈으로 직접 보며 테스트 성공 여부를 확인해야 하고, 

매번 테스트마다 직접 실행시켜줘야 하는 번거로움이 있습니다.

규모가 작을 경우는 어느정도 감수할 수 있지만, 일반적인 웹 사이트 규모만 되도 굉장히 번거롭고 불편해집니다..


main 메서드를 사용한 테스트 예시입니다.

public static void main(String[] args) {       
    ApplicationContext context = new GenericXmlApplicationContext("/applicationContext.xml");
    UserDao dao = context.getBean("userDao",UserDao.class);
    User user1;
    User user2;
    
    dao.deleteAll();
    System.out.println(dao.getCount() == 0);
    
        // ID, 이름, 비밀번호, 이메일
    user1 = new User("test1", "테스트1", "springno1","spring1@spring.book");
    user2 = new User("test2", "테스트2", "springno2","spring2@spring.book");
    
    dao.add(user1);
    dao.add(user2);
    System.out.println(dao.getCount() == 2);

    User userget1 = dao.get(user1.getId());
    System.out.println(userget1.getName().equals((user1.getName())));
    System.out.println(userget1.getPassword().equals((user1.getPassword())));
    
    User userget2 = dao.get(user2.getId());
    System.out.println(userget2.getName().equals((user2.getName())));
    System.out.println(userget2.getPassword().equals((user2.getPassword())));
}

간단히 유저 추가 기능을 검증하기 위한 테스트입니다.

기능별로 묶는것도 좋지만, add, get의 경우 각자 나누기가 더 애매한 경향이 있어서 한대 묶는 테스트가 더 효율적입니다.

그리고 deleteAll을 초반에 실행해줌으로써 항상 동일한 테스트 결과를 보장해주도록 합니다.

※ 테스트는 어떠한 경우에서라도 서로 의존해서는 안되고, 서로 절대 영향을 줘서도 안됩니다.

DB와의 연동도 하나의 테스트 단위로 분류되기 때문에 따로 해줘야 하긴 하지만,

보통 DAO 테스트에서는 쿼리 + DB 연동을 하나로 묶어 테스트합니다.


보다시피 결과값을 사용자가 직접 눈으로 검증해야 한다는 불편함이 있습니다.. true가 다 찍혔으면 테스트 성공이죠.

그리고 한번에 하나의 테스트밖에 실행하지 못한다는 문제점이 있습니다.



JUnit

이미 자바에서는 위와 같은 불편함을 없애기 위한 여러 테스팅 도구들이 존재하고,

그 중 JUnit은 표준이라 할 정도로 폭넓게 사용되는 자바 테스팅 프레임워크입니다.

위의 테스트 코드를 JUnit을 이용한 테스트 코드로 변경해보겠습니다.

@Test
public void addAndGet(){
    ApplicationContext context = new GenericXmlApplicationContext("/applicationContext.xml");
    UserDao dao = context.getBean("userDao",UserDao.class);
    User user1;
    User user2;
    
    dao.deleteAll();
    assertThat(dao.getCount(), is(0));
    
    // ID, 이름, 비밀번호, 이메일
    user1 = new User("test1", "테스트1", "springno1","spring1@spring.book");
    user2 = new User("test2", "테스트2", "springno2","spring2@spring.book");

    dao.add(user1);
    dao.add(user2);
    assertThat(dao.getCount(), is(2));
    
    User userget1 = dao.get(user1.getId());
    assertThat(userget1.getName(), is(user1.getName()));
    assertThat(userget1.getPassword(), is(user1.getPassword()));
    
    User userget2 = dao.get(user2.getId());
    assertThat(userget2.getName(), is(user2.getName()));
    assertThat(userget2.getPassword(), is(user2.getPassword()));
}

바뀐점이 2~3가지 정도 보이실겁니다.


일단은 main 메서드가 없어졌고, 젤 상단에 @Test 라는 어노테이션이 추가되었습니다.

JUnit은 테스팅 프레임워크입니다. 프레임워크의 기본 동작은 IOC 입니다.

즉, 수동적인 상태가 되기때문에 main 메서드가 필요없게 된 것입니다.

그리고 테스트 메서드들 앞에 @Test 어노테이션을 붙여줌으로써, JUnit에 의해 관리되도록 한 것입니다.

결과적으로 하나의 테스트 메서드만 실행할 수 있던 main 메서드의 단점을 던져버렸습니다.

여러 테스트 메서드에 @Test 어노테이션만 붙여놓고 해당 클래스에서 JUnit 딱 실행하면 

@Test 어노테이션 붙은 메서드를 한방에 다 실행해줍니다.

JUnit 테스트 메서드 규칙(@Test)

1. 테스트 대상 메서드가 public 으로 선언되어야 합니다.

2. 메서드에 @Test 라는 어노테이션이 필요합니다.

3. 반환형은 없고, 매개변수도 없어야 합니다.


두번째로 보이시는게 assertThat, is 와 같은 메서드들입니다. 이는 JUnit이 제공하는 테스트용 스태틱 메서드입니다.

assertThat 메서드는 첫번째 파라미터의 값을 뒤에 나오는 matcher라고 불리는 조건과 비교하여

일치하면 다음으로 넘어가고, 아니면 테스트가 실패하도록 만들어줍니다.

is는 matcher의 일종으로 equals()로 비교해주는 기능을 가지고 있습니다.


// JUnit       
assertThat(userget1.getName(), is(user1.getName()));

// main
if(!(userget1.getName().equals((user1.getName())))){
    System.out.println("user1 이름 일치 오류");
    return;
}

하는 행위는 위 2개가 비슷하다고 보시면 됩니다. 하지만 JUnit의 경우가 훨씬 간단하게 작성이 가능합니다.



JUnit 테스트 실행

@Test 메서드가 있는 클래스(테스트 클래스)에 커서를 대고, 아래와 같이 실행합니다.

단축키로도 사용 가능합니다. Alt + Shift + X 누른 뒤, T


콘솔을 통해 일일히 눈으로 확인하던 때와 달리, JUnit view를 통해 한눈에 훨씬 편리하게 확인 가능합니다.

여러개의 테스트를 한번에 실행할 수 있음을 보여드리기 위해 addAndGet 외에도 많은 테스트를 등록해보았고,

한번에 실행되고 있음을 보실 수 있습니다. 

테스트 총 수행시간, 수행한 테스트 개수, 테스트 에러 개수, 테스트 실패 개수 등을 확인할 수 있습니다. 편리하네요


※ Tip. 단일 테스트 실행

JUnit 테스트 실행할 때 메서드 이름에 커서 올리고 JUnit Test 실행하면 그 메서드만 테스트를 실행합니다.


assertThat에서 친절하게 오류 내용을 보여주는 화면입니다. 콘솔에 로그를 찍던 방법보다 훨씬 편리합니다.


프로그램상 오류가 발생했을 경우에도 위와 같이 친절하게 표시해줍니다. 역시 편리합니다..



@Test(expected=~~)

테스트라는 것이 항상 성공만을 테스트하는 것은 아닙니다. 실패 테스트도 있습니다.

@Test(expected=AssertionError.class)
public void getUserFailure() {
    ApplicationContext context = new GenericXmlApplicationContext("/applicationContext.xml");
    UserDao dao = context.getBean("userDao",UserDao.class);
    User user1 = new User("test1", "테스트1", "springno1","spring1@spring.book");

    dao.add(user1);
    
    assertThat(dao.get("test1").getName(), is("테스트2"));  // assertionError 발생
}

원래라면 AssertionError가 발생해서 테스트가 실패해야되지만 성공합니다.

expected에 정의한 예외가 발생하면 테스트가 성공합니다.



@Before, @After

JUnit에서 제공하는 테스팅 어노테이션입니다. 

@Test가 붙은 메서드를 실행하기 전과 후에 각각 @Before과 @After가 붙은 메서드를 자동으로 실행해줍니다.

private ApplicationContext context;
private UserDao dao;
private User user1;
private User user2;

@Before
public void before(){
    context = new GenericXmlApplicationContext("/applicationContext.xml");
    dao = context.getBean("userDao",UserDao.class);
    user1 = new User("test1", "테스트1", "springno1","spring1@spring.book");
    user2 = new User("test2", "테스트2", "springno2","spring2@spring.book");
    System.out.println("Before");
}

@After
public void after(){
    System.out.println("After");
}

이런식으로 테스트 메서드마다 중복되던 ApplicationContext와 User 세팅 부분을 @Before로 빼줄수 있습니다.

(@After 부분에는 마땅히 할게 없어서 그냥 문자열만 찍었습니다...)

@Test 메서드에서 @Before, @After를 호출해서 사용하는 방식이 아니기 때문에, 공유해야 하는 변수는 인스턴스 변수를 사용해야 합니다.

이후 테스트를 실행해보시면 테스트 개수만큼 "Before", "After" 문자열이 찍힘을 보실 수 있습니다.


JUnit이 하나의 테스트 클래스를 실행하는 절차는 간단하게 아래와 같습니다.

1) 테스트 클래스의 오브젝트를 하나 만든다

2) @Before 메서드를 실행한다.

3) @Test 메서드를 실행하고 테스트 결과를 저장해둔다.

4) @After 메서드를 실행한다.

5) @Test 메서드의 개수만큼 1~4를 반복한다. 

6) 모든 테스트의 결과를 종합해서 돌려준다.

@Test 메서드마다 오브젝트를 각각 만든다는 점을 알고 계셔야합니다. 하나의 오브젝트를 만들어 공유하면 편리하지만

각 테스트마다 서로 영향을 주지 않고 독립적으로 실행됨을 보장해주기 위해 매번 새로운 오브젝트를 만들어줍니다.




@RunWith, @ContextConfiguration

테스트 클래스에서 applicationContext에 있는 빈들을 사용할 때의 불편함을 해소해주는 어노테이션입니다.

@Before 어노테이션을 통해 테스트 메서드마다 applicationContext를 생성하는 번거로움을 제거했지만,

JUnit은 테스트 메서드마다 오브젝트를 각각 생성하여 실행하기 때문에, 테스트 메서드의 개수만큼 applicationContext가 생성되는 단점이 있습니다.

이를 위해 JUnit에서는 클래스 전체에 걸쳐 딱 한번 실행되는 @BeforeClass 어노테이션을 제공하지만, 이보다는

스프링에서 제공하는 테스트 컨텍스트 프레임워크를 사용하는 편이 낫습니다.


@RunWith(SpringJUnit4ClassRunner.class)

@ContextConfiguration(locations="/aplicationContext.xml")

SpringJUnit4ClassRunner라는 JUnit용 테스트 컨텍스트 프레임워크를 사용하겠다는 의미이고,

@ContextConfiguration을 통해 테스트 컨텍스트 프레임워크가 사용할 설정파일을 지정해주는 것입니다.

간단하게 얘기해서, 지정한 설정파일로 applicationContext가 하나 생성되어 해당 테스트에서 사용 가능하게 됩니다.


테스트 클래스에서는 스프링에서 제공하는 @Autowired 어노테이션을 이용하여 더 간편하게 빈을 가져올 수 있습니다.

public class UserDaoTest {
	@Autowired
	private UserDao dao;

        public void add{
            dao.add(user1);  // 초기화 없이 바로 사용 가능
            dao.add(user2);
        }

        ....
}

변수를 선언하고 상단에 @Autowired를 선언해주면, 위에서 지정한 설정파일 내에서 UserDao와 같은 타입의 빈을 찾은 뒤,

자동으로 주입합니다. (Type-driven 입니다)

위와 같이만 선언해놓고, 아래의 테스트 메서드들에서 dao.add(~~), dao.deleteAll() 등을 사용해도 전혀 

NullPointerException이 발생하지 않을 것 입니다.


위와 같은 방식을 사용할 때, 테스트 클래스에서 2가지 종류의 컨텍스트 공유를 보실 수 있습니다.


1. 테스트 클래스에서 컨텍스트를 공유합니다.

> JUnit은 테스트 독립성을 위해 @Test 메서드별로 각각 오브젝트를 생성한다고 했습니다.

@Before에서 new GenericXmlApplicationContext(~~)와 같은 초기화 작업이 테스트 메서드마다 반복되었습니다.

즉, 매번 새로운 컨텍스트를 만들었다는 뜻이죠.

하지만 SpringJUnit4ClassRunner를 사용할 경우 컨텍스트를 한번만 만들게 되고, 테스트 메서드들 끼리 서로 공유합니다.


2. 테스트 클래스들 끼리도 컨텍스트를 공유합니다.(설정파일의 위치가 같을 경우)

컨텍스트는 메서드들끼리의 공유를 넘어서서, 클래스 끼리도 공유가 가능합니다.

예를 들어, UserDaoTest와 CarDaoTest의 테스트 컨텍스트 프레임워크가 같은 설정파일을 사용할 경우

두 테스트 클래스는 같은 컨텍스트를 공유하게 됩니다.




@DirtiesContext

위에 노란색에서 말씀드린 것 처럼, 테스트 컨텍스트 프레임워크를 사용하여 컨텍스트를 생성했을 경우

해당 컨텍스트는 싱글톤 처럼 서로 공유 가능한 컨텍스트가 됩니다.


그런데 일부 테스트에서 컨텍스트의 내용을 조금 변경해야 할 경우가 생기면 어떻게 될까요?

아래와 같이 로컬 DB를 사용하는 테스트가 있을 수 있죠.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations="/aplicationContext.xml")
public class UserDaoTest_localDB {
	@Autowired
	private UserDao dao;
	
	@Before
    public void before() {
	    DataSource dataSource = // 테스트 DB DataSource 생성
	    dao.setDataSource(dataSource);
    }

    ....
}

DB정보의 변경이 있기 때문에 dataSource를 바꿔주는 작업이 있습니다.

허나 테스트 컨텍스트 프레임워크를 사용할 경우 컨텍스트를 공유하는 점을 위에서 언급했었죠..

같은 설정파일을 사용하는 테스트의 경우 컨텍스트를 다른 클래스들도 공유하게 되는데, 위와 같이 컨텍스트 정보를 변경해주면 다른 테스트 클래스에도 영향을 미치게 됩니다.

그래서 여기서 사용되는 것이 @DirtiesContext 입니다. 

이 어노테이션은, 해당 클래스는 컨텍스트를 공유하지 않겠다 라는 의미를 가지게 됩니다.

따라서, @DirtiesContext 어노테이션을 선언한 클래스에서 컨텍스트의 변형이 발생해도, 

다른 테스트에는 영향을 미치지 않게 됩니다.




감사합니다!

'Spring' 카테고리의 다른 글

예외처리, 스프링 예외처리  (0) 2016.07.05
템플릿 콜백 패턴  (0) 2016.06.28
테스트와 JUnit  (1) 2016.06.27
싱글톤과 스프링  (0) 2016.06.24
스프링 DI  (0) 2016.06.22
DI란?  (0) 2016.06.16

댓글을 달아 주세요