2014년 2월 25일 화요일

JUnit을 이용한 단위 테스트

1. JUnit 이란

JUnit은 자바용 단위 테스트 작성을 위한 산업 표준 프레임워크다.

2. JUnit 환경 세팅

JUnit개발 가이드는 이클립스 + springMVC + maven 개발환경 기반으로 작성하였다.
혹 위 환경기반으로 프로젝트를 작성하지 않았다면 아래 개발환경 구축 내용을 확인하기 바람
이클립스 + 톰캣 스프링MVC + maven 개발환경 구축 1장
이클립스 + 톰캣 스프링MVC + maven 개발환경 구축 2장
이클립스 + 톰캣 스프링MVC + maven 개발환경 구축 3장


2.1 JUnit 라이브러리 추가
JUnit을 사용하려면 프로젝트에 JUnit 라이브러리가 필요하다.
Maven프로젝트는 의존관계 설정이 쉽게 되어 기존 프로젝트에서 처럼 개발자가 해당 라이브러리를 찾는 수고를 덜어준다.
Project Object Model(POM.xml) 에서 아래 그림1과 같이 Dependencies탭에서 JUnit을 찾아 추가를 하면 된다.
그림1


또는 직접 POM.xml에 dependencies element에 JUnit dependency를 아래와 같이 직접 추가할 수도 있다.
<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.7</version>
  <scope>test</scope>
</dependency>


위와 같이 추가를 하게 되면 Maven 프로젝트 Install시 해당 라이브러리가 그림2와 같이 로컬 저장소에 저장하게 된다.
그림2


그림3
2.2 프로젝트 패키지 구성
JUnit테스트를 하기 위해서는 테스트 대상 클래스와 테스트 클래스는 같은 패키지 내에 있어야 한다. Maven 프로젝트를 생성하게 되면 Maven 관례에 따라, 그림3과 같은 프로젝트 템플릿이 기본적으로 생성된다. 그림3 디렉토리  /src/main/java/는 자바 코드를 보관하고
단위 테스트의 소스는 디렉토리 /src/test/java/ 디렉토리에 보관한다.
테스트 하고자 하는 클래스가 포함된 패키지명과 동일하게 테스트 클래스를 포함하는 패키지도 동일하게 구성한다. 테스트 대상 클래스와 테스트 클래스의 생성 예이다.



3. JUnit 테스트 클래스 작성

3.1 간략한 계산기 클래스 테스트

그림3의 프로젝트 패키지 구성에서 /src/main/java/ 디렉토리에
Calurator.java 클래스를 아래와 같이 작성한다.
package com.y2kpooh.junitTest;

public class Calcurator {
    public double sum(double a, doubleb){
        return a + b;
    }
}


위 Calcurator클래스는 double형의 두개의 파라메터를 받아 두 파라메터의 합을 구하여 double형으로 리턴해주는 sum 메서드를 가지고 있다.
물론 위 클래스는 문제가 될리 없는 간단한 프로젝트이나 테스트 클래스 작성에 이해를 돕기 위함이다.

Calurator클래스 작성 후 해당 클래스를 테스트 하기 위한 테스트 클래스를 작성해보자.
그림3의 프로젝트 패키지 구성에서 /src/test/java/ 디렉토리에 CaluratorTest.java 클래스를 아래와 같이 작성한다.
package com.y2kpooh.junitTest;

import org.junit.Test;
import static org.junit.Assert.*;

public class CaluratorTest {                                                  ←
    @Test                                                                                 
    public void testSum(){                                                        
        Calcurator c = new Calcurator();
        double result = c.sum(10, 50);                                     ←
        assertEquals(60, result, 0);                                           ←
    }
}


테스트 클래스는 반드시 public으로 선언해야 하며 클래스명은 관례에 따라 테스트클래명 + Test 끝나는 이름으로 사용된다. JUnit 3에서는 TestCase클래스를 상속받아 사용해야 했으나 JUnit 4에서는 상속받지 않아도 된다. (이 문서는 JUnit 4를 기반으로 작성되었다.)
@Test 어노테이을 선언하여 testSum 메서드가 단위 테스트 메서드임을 선언하였다.
클래스명과 마찬가지로 테스트 메서드는 test + 테스트메서드명으로 선언한다. @Test 어노테이션을 선언한 메서드는 JUnit이 알아서 실행을 해준다.
Calcurator 클래스의 인스턴스를 선언하여 sum 메서드에 10, 50 인자값을 세팅하여 result변수에 결과값을 리턴 받는다.
JUnit 프레임워크에의 Assert 클래스의 정적 메서드인 assertEquals를 이용하여 테스트 결과 값을 확인한다. assertEquals(expected, actual, delta)는 assertEquals(예상값, 실제값, 허용오차)

CalcuratorTest 클래스 테스트 결과 그림4와 같이 테스트 성공 결과가 나온다.
그림4

3.2 JUnit assert 주요 메서드 및 사용예시
assert 메서드
설명
assertArrayEquals(a, b);배열 A와 B가 일치함을 확인한다.
assertEquals(a, b);객체 A와 B가 일치함을 확인한다.
assertSame(a, b);객체 A와 B가 같은 객임을 확인한다. assertEquals 메서드는 두 객체의 값이 같은가를 검사는데 반해 assertSame메서드는 두 객체가 동일한가 즉 하나의 객인 가를 확인한다.(== 연산자)
assertTrue(a);조건 A가 참인가를 확인한다.
assertNotNull(a);객체 A가 null이 아님을 확인한다.

위 메서드 외에도 많은 메서드와 오버로드된 메서드를 제공한다.
자세한 내용은 http://junit.sourceforge.net/javadoc/org/junit/Assert.html 해당 링크를 참고
String names[] = {"y2kpooh","hwang"};
String names2[] = {"y2kpooh","hwang"};
assertArrayEquals(names2, names);

List someList = someClass.getSomeList();
assertNotNull("조회결과 null", someList);
assertTrue(someList.size() > 0);
assertEquals(3, someList.size());



3.3 JUnit Annotation 사용 예시

스프링 프레임워크 기반의 JUnit 테스트를 위한 세팅
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={"file:WebContent/WEB-INF/classes/applicationContext*.xml"})

Spring 기반의 테스트 코드 작성을 위해 테스트 클래스 상단에 @RunWith(SpringJUnit4ClassRunner.class) 구문을 추가한다.
Spring 프레임워크 context 파일을 테스트 수행시에도 동일하게 로딩하기 위해 @ContextConfiguration(locations={"file:WebContent/WEB-INF/classes/applicationContext*.xml"}) 과 같은 형태로 프로젝트의 스프링 설정파일을 설정해 준다.

메서드 수행시간 제한하기
@Test(timeout=5000)

단위는 밀리초이며 이 메서드가 결과를 반환하는데 5,000밀리초가 넘긴다면 테스트는 실패한다.

Exception 테스트
@Test(expected=RuntimeException.class)

해당 클래스는 RuntimeException이 발생해야 한다. 만약 테스트에서 RuntimeException이 발생하지 않을 경우 실패한다.

테스트 건너뛰기
@Test(timeout=5000)
@Ignore(value=”여기는 테스트 안할거야”)

@Ignore 어노테이션을 추가하면 해당 메서드는 테스트를 건너뛰게 되며 JUnit4는 성공 및 실패 개수와 함께 건너뛴 테스트 수도 포한된 결과 통계를 제공한다.



초기화 및 종료
@Before
[...]
@After
[...]

@Before 어노테이션이 선언된 메서드는 해당 테스트 클래스의 인스턴스, 객체를 초기하 하는 작업을 한다. @After 어노테이션이 선언된 메서드는 해당 테스트 실행 후 실행된다.
해당 @Before, @After 어노테이션 이외 @BeforeClass, @AfterClass도 있으며 이는 static 메서드와 동일한 형태로 테스트 클래스 실행 시 한번만 실행된다.


4. 목 객체를 활용한 테스트

4.1 목(Mock) 객체란?
 어플리케이션플리케이션을 개발하다보면, 테스트 대상 코드가 다른 클래스에 종속되어 있을 때가 종종 있다. 그 클래스가 다시 다른 클래스에 종속되고, 역시 또 다른 클래스에 종속되기도 한다.
JDBC를 통해 데이터베이스에 의존하는 JAVA EE 애플리케이션, 파일 시스템을 사용하는 어플리케이션, HTTP나 SOAP 등의 프로토콜로 외부 리소스에 접근하는 어플리케이션들을 예로 들 수 있다.
특정 런타임 환경에 의존적인 어플리케이션을 단위 테스트하는 것은 꽤나 고된 일이다.
테스트는 안정적이어야 하고, 반복적으로 수행해도 매번 동일한 결과를 내야 한다. 따라서 테스트를 올바로 수행하기 위해서는 환경을 제어할 수 있어야 한다.
예를 들어 다른 회사에 제공하는 웹 서버에 HTTP 커넥션을 맺는 어플리케이션을 제작하는 경우 그 외부 서버를 개발 환경 안에 가져올 수 없다. 테스트를 작성하고 돌려 보려면, 결국 서버를 시뮬레이션하는 방법이 필요하다.
또는 팀 단위 프로젝트에서 내가 맡은 부분을 테스트해보려 할때 다른 부분이 아직 준비되지 않았다면... 가짜를 만들어 미처 준비되지 못한 부분을 시뮬레이션할 수 있다.
이 처럼 가짜 객체를 제공하는 테스트 방식으로 목 객체를 사용할 수 있다.(스텁방식도 있다.)

4.2 목 객체를 활용해 단위 테스트 하기
한 은행 계좌에서 다른 계좌로 송금하는 단순한 테스트 케이스이다.

위 계좌 송금 프로세스를 기능 테스트 하기 위해서는 AccountService를 테스트 하기 위해서는 우선 데이터베이스를 세팅한 후 테스트 데이터를 채워넣는 작업을 진해하여야 한다.
목 객체를 활용하면 아직 작성되지 않은 코드를 테스트할 수 있다.
단 인터페이스가 정의되어 있어야 한다.

Account.java
계좌 ID와 잔고를 갖는 Account 객체
package com.y2kpooh.mock;

public class Account
{
   /**
    * 계좌 아이디
    */
   private String accountId;

   /**
    * 계좌 잔고
    */
   private long balance;

   /**
    * 초기화
    *
    * @param accountId
    * @param initialBalance
    */
   public Account( String accountId, long initialBalance )
   {
       this.accountId = accountId;
       this.balance = initialBalance;
   }

   /**
    * 출금
    *
    * @param amount
    */
   public void debit( long amount )
   {
       this.balance -= amount;
   }

   /**
    * 입금
    *
    * @param amount
    */
   public void credit( long amount )
   {
       this.balance += amount;
   }

   /**
    * 현재 잔고
    *
    * @return
    */
   public long getBalance()
   {
       return this.balance;
   }
}


AccountManager.java 인터페이스
Account 객체의 생명주기와 영속성을 관리한다.
package com.y2kpooh.mock;

public interface AccountManager
{
   /**
    * 아이디로 계좌 계정찾기
    *
    * @param userId
    * @return
    */
   Account findAccountForUser( String userId );

   /**
    * 계좌 계정 업데이트
    *
    * @param account
    */
   void updateAccount( Account account );
}


AccountService.java
두 계정 사이의 송금 기능을 제공한다. ID로 돈을 찾을 계좌와 받을 계좌를 찾고 정보를 갱신하기 위해 앞서 정의한 AccountManager 인터페이스를 활용한다.
package com.y2kpooh.mock;

public class AccountService
{
   /**
    * AccountManger 인터페이스 선언
    */
   private AccountManager accountManager;

   /**
    * 객체 초기화
    *
    * @param manager
    */
   public void setAccountManager( AccountManager manager )
   {
       this.accountManager = manager;
   }

   /**
    * 두 계좌 사이 송금기능
    *
    * @param senderId
    * @param beneficiaryId
    * @param amount
    */
   public void transfer( String senderId, String beneficiaryId, long amount )
   {
       Account sender = this.accountManager.findAccountForUser( senderId );
       Account beneficiary = this.accountManager.findAccountForUser( beneficiaryId );

       sender.debit( amount );
       beneficiary.credit( amount );
       this.accountManager.updateAccount( sender );
       this.accountManager.updateAccount( beneficiary );
   }
}


MockAccountManager.java
AccountService.transfer 메서드를 단위 테스트하고자 하므로 이를 위해 AccountManger 인터페이스의 목 객체를 구현해야 한다.
package com.y2kpooh.mock;

import java.util.Map;
import java.util.HashMap;

public class MockAccountManager implements AccountManager
{

   private Map<String, Account> accounts = new HashMap<String, Account>();

   /**
    * 아이디와 account 객체를 HashMap객체에 put
    *
    * @param userId
    * @param account
    */
   public void addAccount( String userId, Account account )
   {
       this.accounts.put( userId, account );
   }

   /**
    * 아이디로 HashMap객체에서 account 객체를 찾아 리턴
    */
   public Account findAccountForUser( String userId )
   {
       return this.accounts.get( userId );
   }

   /**
    * 계정 정보를 갱신하며 반환값은 없다.
    */
   public void updateAccount( Account account )
   {
       // do nothing
   }
}


TestAccountService.java
MockAccountManger를 이용하여 transfer 테스트하기
package com.y2kpooh.mock;

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class TestAccountService
{
   @Test
   public void testTransferOk()
   {
       //테스트를 하기위한 객체 생성 및 준비
       MockAccountManager mockAccountManager = new MockAccountManager();
       Account senderAccount = new Account( "1", 200 );  
       Account beneficiaryAccount = new Account( "2", 100 );
       mockAccountManager.addAccount( "1", senderAccount );
       mockAccountManager.addAccount( "2", beneficiaryAccount );
       
       AccountService accountService = new AccountService();
       accountService.setAccountManager( mockAccountManager );
       // 테스트 수행
       accountService.transfer( "1", "2", 50 );
       // 결과 검증
       assertEquals( 150, senderAccount.getBalance() );
       assertEquals( 150, beneficiaryAccount.getBalance() );
   }
}


테스트 결과 및 프로젝트 패키지 구성화면
그림5



4.3 목 프레임워크 활용하기
목 객체를 활용하여 테스트 하려면 목 객체를 직접 개발자가 만들어야 한다.
바쁜 프로젝트 일정에 테스트하려고 목 객체를 만들자니 배보다 배꼽이 큰 것 같은 생각이 들지도 모른다. 역시나 천재들이 만들어 놓은 훌륭한 목 프레임워크가 존재 한다.
EasyMock과 JMock이 있으며 해당 라이브러리만 세팅하면 쉽게 목 객체를 활용할 수 있다.
<dependency>
  <groupId>org.easymock</groupId>
  <artifactId>easymock</artifactId>
  <version>3.0</version>
</dependency>


테스트를 하고자 하는 클래스 AccountService는 이미 완성되어 있으며 해당 클래스를 테스트 하기 위해서는  AccountManager에 대한 Implement Class가 없다. 이 AccountManager에 대한 클래스를 EasyMock을 이용해서 테스트 가능하다.
(4.2 목 객체를 이용한 테스트 케이스에서는 AccountManager의 Implement Class로 MockAccountManager 클래스를 작성하여 테스트가 가능했었다. 여기서 꼭 기억할 것은 목 객체를 이용하여 테스트하기 위해서는 꼭 인터페이스가 정의되어 있어야 한다.)

easymock을 이용한 테스트 클래스 작성
package com.y2kpooh.mock;

import static org.junit.Assert.assertEquals;
import static org.easymock.EasyMock.createMock;
import static org.easymock.EasyMock.replay;
import static org.easymock.EasyMock.expect;
import static org.easymock.EasyMock.verify;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

public class TestAccountServiceEasyMock
{
   private AccountManager mockAccountManager;

   @Before
   public void setUp()
   {
       //목 객체를 생성한다.
       mockAccountManager = createMock("mockAccountManager", AccountManager.class );
   }

   @Test
   public void testTransferOk()
   {
       Account senderAccount = new Account( "1", 200 );
       Account beneficiaryAccount = new Account( "2", 100 );

       mockAccountManager.updateAccount( senderAccount );
       mockAccountManager.updateAccount( beneficiaryAccount );

       // 기대되는 행위 및 리턴 값 기록 한다.
       // expect : 기대되는 행위 메서드
       // addReturn : 리턴
       expect( mockAccountManager.findAccountForUser( "1" ) ).andReturn( senderAccount );
       expect( mockAccountManager.findAccountForUser( "2" ) ).andReturn( beneficiaryAccount );
       // 해당 목 객체를 수행한다.
       replay( mockAccountManager );

       AccountService accountService = new AccountService();
       accountService.setAccountManager( mockAccountManager);
       accountService.transfer( "1", "2", 50 );

       assertEquals( 150, senderAccount.getBalance() );
       assertEquals( 150, beneficiaryAccount.getBalance() );
   }

   @After
   public void tearDown()
   {
       // 테스트 실행
       verify( mockAccountManager);
   }
}
easymock에 대해서 더 자세히 알고 싶으시다면 아래 사이트를 참고하시기 바랍니다.
http://openframework.or.kr/framework_reference/easymock/2.3/Documentation_ko.html


                                                                               그림6

댓글 없음: