성공은열정을잃지않고실패를거듭하는와중에나온다. - 윈스턴처칠
CHAPTER 2 JUnit 과 Hamcrest 앞서진행한예제에선 JUnit 테스트프레임워크에대한자세한설명없이곧바로사용했었다. 이제부터는 JUnit 에대해좀더자세히살펴볼까한다. JUnit 은 Java 언어에서 TDD나단위테스트를이야기할때빼놓고이야기하기어려울정도로절대적인위치를차지하고있는단위테스트프레임워크다. 비록깊숙한내용까지는모르더라도한번은꼭배워둘필요가있다. 그리고테스트코드내에서는 assertequals 같은비교문장이종종사용되는데, 등호, 부등호비교수준의간략한형태의비교에는한계가있다. 또한부등호비교나객체안의값찾기, 문자열안의특정값찾기등의좀더다양한판단조건이필요한경우에는, 자칫문장이복잡해져서판단조건이무엇인지혼란스러워지기쉽다. 이럴때 Hamcrest 라는라이브러리를함께사용하면, 좀더이해하기쉽고우아한비교문장을만들수있다. JUnit 을사용해왔던사람들조차 Hamcrest 에대해서는잘모르거나낯설게느껴질수있는데, 현재 Hamcrest 라이브러리는 JUnit 4 버전에포함되어함께배포되고있다. Hamcrest 는무엇이고, 어떤식으로비교문장을좀더자연스럽게만들어주며, JUnit 과는어떤식으로함께사용하게되는지에대해서도이번장에서살펴볼예정이다. 자, 그럼 JUnit 부터살펴보기로하자. 2 장체크리스트 테스트픽스처의개념 JUnit 3 단정문 테스트스위트 2 장 JUnit 과 Hamcrest 95
JUnit 4 @Test @BeforeClass, @AfterClass, @Before, @After 예외처리테스트 시간제한테스트 @Runwith @SuiteClasses 고급기능소개 파라미터화된테스트 Rule Theory Hamcrest 2.1 JUnit 에릭감마와켄트벡이탄생시킨 JUnit 은현재전세계적으로가장널리사용되는 Java 단위테스트프레임워크다. TDD의근간이되는프레임워크이며, 소위 xunit 시리즈 1 라고불리는다양한단위테스트프레임워크들의기원이되는프레임워크다. 전체소스가공개되어있는오픈소스프로젝트 2 방식으로개발되며지금도꾸준히버전업을하고있다. JUnit 은제공하는기능도기능이지만, 만들어진방식이나구조자체에서도 1 JUnit 은다양한언어로포팅됐는데, 포팅된단위테스트프레임워크는실행구조가유사하며, 일반적으로 Unit 이라는단어앞에프로그래밍언어의이름일부를붙이고있다. Java 는 JUnit, C는 CUnit, C++ 는 CppUnit, Python 은 PyUnit, 닷넷은 NUnit 같은식으로말이다. 이런프레임워크들을통칭 xunit 프레임워크라고한다. 2 JUnit 은 CPL(Commons Public License) 1.0을따른다. CPL 라이선스제품은, 해당제품혹은제품의일부를이용해개인소프트웨어 / 상용소프트웨어구분없이만들수있다. 라이선스관련해서는 http://www.codeproject.com/info/ Licenses.aspx 에종류별로자세히설명되어있으니참고하자. 96 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
배울점이매우많은소프트웨어다. JUnit 은단위테스트를수행하는데있어기본적으로다음과같은기능을제공한다. - 테스트결과가예상과같은지를판별해주는단정문 (assertions) - 여러테스트에서공용으로사용할수있는테스트픽스처 (test fixture) - 테스트작업을수행할수있게해주는테스트러너 (test runner) JUnit 이널리퍼진이유중하나는위에서보는것과같이목표가단순하고, 하고자하는일이명료했기때문이다. 거기에쉬운사용방법도크게한몫거들었다 ( 물론최신버전은머리에스팀이살살올라오는다소복잡한기능들을포함하고있지만말이다 ). JUnit 을비롯하여단위테스트케이스를만드는데있어꼭알아둘개념이하나있는데, 테스트픽스처 ( 테스트기반환경또는테스트를위한구조물 ) 라는개념이다. 테스트픽스처일반적으로소프트웨어테스트에서이야기하는 테스트픽스처 란테스트를반복적으 로수행할수있게도와주고매번동일한결과를얻을수있게도와주는 기반이되는상태나환경 을의미한다. 일관된테스트실행환경이라고도하며, 때로는테스트컨텍스트 (test context) 라부르기도한다. 테스트케이스에서사용할객체의인스턴스를만든다든가, 데이터베이스와연동할수있는참조를선언한다든가, 파일이나네트워크등의자원을만들어지정한다든가하는등의작업혹은그작업의결과로만들어진대상을통칭한다. 1장예제에서사용한 setup 이나 teardown 메소드는테스트픽스처를만들고, 정리하는작업을수행하는메소드인데, 이런메소드를테스트픽스처메소드 (test fixture method) 라고한다. 테스트케이스와테스트메소드 테스트케이스 (test case) 와테스트메소드 (test method) 라는용어는흔히혼용되어사용된다. 정확히는단위테스트케이스와단위테스트메소드가제대로된명칭이다. 2 장 JUnit 과 Hamcrest 97
테스트케이스는테스트작업에대한시나리오적인의미가더강하고테스트메소드는 JUnit 의메소드를지칭한다. 그러나테스트케이스가곧테스트메소드인경우가많기때문에두단어는종종혼용되어사용된다. 앞으로동일한뜻으로함께사용하더라도크게혼란스러워하지않길바란다. JUnit의기원켄트벡과에릭감마의이름을어느정도들어봤는지모르겠다. 두사람다이분야에선매우유명한거물에속한다. 한사람은 IT 세계를크게움직인 XP와 TDD의창시자이고, 다른한사람은역사상가장크게흥행한 IT 서적이라불리는 GoF의디자인패턴 의저자네명중한명이다. 이두사람에의해만들어진 JUnit의기원은이러하다. 시절은바야흐로 1997년으로돌아간다. 그당시켄트벡은 CSLife라는이름모를회사에서 Smalltalk 전문가로일하고있었고, 에릭은 OTI라는 IBM 산하연구소에서일하는프로그래머였다. 둘은강연때문에애틀랜타행비행기를함께타게되는데, Java를써본적이없는켄트벡은에릭에게 Java를가르쳐달라고했고, 마침켄트벡의 SUnit( 스몰토크테스트프레임워크 ) 에평소관심이있었던에릭은 SUnit의 Java 버전을함께만들어보기로한다. 그래서세시간남짓한시간동안비행기안에서노트북으로함께코딩을하게됐고, 그때나온프로그램이 JUnit 테스트프레임워크의모태가된다. 만일 JUnit의기원을좀더자세히알아보고싶다면, http:// members.pingnet.ch/gamma/ 로찾아가보기바란다. JUnit 1.0의소스를다운로드받을수있고, 켄트벡과에릭이쓴 Test Infected 라는 TDD 전파의신호를알리는기념비적인 TDD 소개글도볼수있다. 참고로 2010년현재켄트벡은 Three Rivers Institute 라는개인회사를차려부인과함께컨설팅활동을하고있고, 에릭은 IBM에서 Distinguished Engineer 라는직함으로여전히엔지니어생활을하고있다. 그런데순수엔지니어인에릭과달리켄트벡은, 오리건에있는자신의농장에서표범과맹수인쿠거 (Cougar) 와싸우고염소젖을짜서치즈를만드는일을컨설팅업무와겸업으로하고있다. 아.. 정말이다. 98 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
1997 년에두사람이 JUnit 1.0 을만들면서작성한글, 테스트에중독되다 : 프로그래머들은테스트작성하는걸사랑한다. 3 2.1.1 JUnit 3 원로급 Java 개발자가아니라면, 대부분처음접한 JUnit 은버전 3( 이하 JUnit 3) 일것이다. 4 지금은 JUnit 4가주로사용되고있지만, 아직 JUnit 3을사용하고있는프로젝트들도상당수남아있다. JUnit 은 junit.org 사이트에서파일을내려받아서클래스경로내에 junit.jar 파일을포함시켜놓으면바로사용할수있다. 설치가쉽고몇분정도만시간을들여설명을읽으면대부분의기능을무리없이사용할수있을정도로사용법이간단하다. 3 이글은다음과같은문장으로시작한다. 테스트하는일과개발은밀접하게통합되어있지가않다. 그렇기때문에개발진척을측정하기가어렵다. ( 누가물어보더라도 ) 일이언제시작해서언제끝날지말할수없을것이다. JUnit 을사용하면적은비용으로, 그리고점차 ( 케이스가 ) 증가되는형태로테스트집합을만들어낼수있다. 그리고그테스트집합은개발진척이어떻게되는지를알려줄수있다. 또한 JUnit 을사용하면의도하지않았던부작용을발견해낼수있고, 개발업무에좀더집중할수있다. 4 JUnit 1은 97년에만들어져서 98년에발표됐고, JUnit 3 버전은 99년 11월에공개된이후로지금까지사용되고있다. 2 장 JUnit 과 Hamcrest 99
JUnit 3 프레임워크는테스트케이스를작성하는데있어, 크게두개의규칙과네개의구성요소를갖는다. JUnit 3 규칙 규칙 1 TestCase를상속받는다. 예 : AccountTest extends TestCase 규칙 2 테스트메소드의이름은반드시 test로시작해야한다. 예 : testgetaddress( ), testcalculatevalue( ) 등 예 public class CalculatorTest extends TestCase { public void testcalculatesum() { Test Fixture Method JUnit 3 구성요소 1 테스트픽스처메소드 - setup( ) - teardown( ) 테스트클래스의메소드실행순서 100 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
JUnit 3에서는 setup 과 teardown 이라는두개의테스트픽스처메소드를지원하고있다. setup 은각각의테스트메소드가실행되기전에공통으로호출되는메소드다. 보통테스트환경준비에해당하는, 자원할당, 객체생성, DB 연결이나네트워크연결등의작업이이뤄진다. teardown 은테스트메소드가수행된다음수행되는메소드다. setup 과는반대로자원해체, 연결해제, 객체초기화등의뒷정리작업을하게된다. setup 과 teardown 을사용할때는대소문자에유의하자. 오버라이드된형태로실행되기때문에이름이다르면실행이안된다. ( 너무당연한건가?) JDK 1.5 이상을사용한다면, @Override 애노테이션을붙여서컴파일러가강제로체크할수있도록유도할수도있다. import junit.framework.testcase; public class DaoTest extends TestCase { Connection connection; protected void setup() throws Exception { connection = Connection.getConnection(); public void testa() throws Exception { public void testb() throws Exception { protected void teardown() throws Exception { account = Connection.releaseConnection(); 위코드는 setup testa teardown, setup testb teardown 식으로실행된다. JUnit 3 구성요소 2 단정문 Assertions 대표적인단정문 - assertequals( [message], expected, actual ) - asserttrue( [message], expected ) / assertfalse( [message], expected ) 2 장 JUnit 과 Hamcrest 101
- assertnull( [message], expected ) / assertnotnull( [message], expected ) - fail( [message] ) * [message] 는선택사항임 JUnit 은테스트케이스의수행결과를판별해주는다양한단정문 ( 무언가를딱잘라한마디로판단하는짧은문장 ) 을제공한다. 일반적으로 단언하다 는뜻의 assert 라는단어로시작하며 두값비교, 참 / 거짓, null 여부 등을판별한다. 두값의일치여부를판단하는 assertequals 를그중에서제일많이사용한다. assertequals([message], expected, actual) 두값이같은지비교하는단정문으로, 예상에해당하는 expected 와실제테스트수행결과에해당하는 actual 이서로일치하는지비교판단한다. expect 와 actual 은 Java 언어의기본타입 (primitive type) 전체에대해중첩구현 (overloading) 되어있기때문에다양한값을서로비교할수있다. 만일비교값이기본타입이아닐경우에는 equals 메소드비교를하게되어있다. 따라서 equals 를구현해놓은클래스라면, 해당 equals 메소드를호출해서비교에사용한다. 다음은 JUnit 에서 assertequals 를정의한부분이다. 두값을비교하기위해어떤방식을취하고있는지살짝살펴보자. static public void assertequals(string message, Object expected, Object actual) { if (expected == null && actual == null) return; if (expected!= null && expected.equals(actual)) return; failnotequals(message, expected, actual); 둘다 null 이면통과! expected 가 null 이아니고 expected 의 equals 메소드를이용해비교했을때같으면또통과! 그리고위두경우가아니면실패 (fail) 로처리된다. 102 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
assertequals 시리즈중모양이다소다른것이있는데, 바로 assertequals ([message], double expected, double actual, double delta) 메소드다. 메소드인자 (argument) 마지막에 delta 라는항목이하나더있다. 눈치빠른독자라면바로 아하! 할텐데, 소수점을갖는 float 나 double 데이터형의경우에는정확하게일치하는값을찾기어려울수있다. 그럴경우 delta 라는오차보정값을이용해적절한오차범위내의값은동일한값으로판단할수있게해준다. assertequals(0.3333, 1/3d, 0.00001); 위경우는 1을 3으로나눈 double 타입의예상값으로 0.3333 을지정했다. 소수다섯째자리에서 1 이하의오차를갖는다면일치하는것으로간주한다. 오차 delta 허용치이상으로값이차이가나면테스트가실패한다. 이럴경우 delta 값의자릿수를아래와같이높이면, 테스트가성공한다. assertequals(0.3333, 1/3d, 0.0001); JUnit 구버전에는 double 타입을비교할때오차값을지정하지않는 assertequals (double expected, double actual) 이라는메소드가있었는데지금은권장하지않음 (deprecated) 으로처리되어사용하지않는다. 그리고앞에서 assertequals 의다양 2 장 JUnit 과 Hamcrest 103
한타입을중첩구현 (overloading) 해놓았다고이야기했었는데, float 타입끼리의비교는제공하지않는다. 소수를표현하는 float 와 double 은서로호환되는타입인데다, double 쪽이훨씬더큰숫자와높은정밀도를사용할수있기때문이다. 참고로 float 는 4바이트의저장영역에 2 23 의정밀도를갖고, double 은 8바이트저장소에 2 52 의정밀도영역을갖는다. assertsame([message], expected, actual) assertnotsame([message], expected, actual) assertsame 은두객체가정말동일한객체인지주소값으로비교하는단정문이다. 따라서객체를비교할때 equals 메소드를사용하지않고바로등가비교 (==) 를한다. assertnotsame 도마찬가지로두객체를주소로비교한다. 다만이경우주소값이다르면무조건 true 가된다. 어! 그런데 assertsame 계열단정문은앞에서박스로된소개에없었는데? 이거혹시오타? 그건아니고같이쓰면복잡해보일것같아서빼봤다. 에엣? 이건뭔풀뜯어먹는소리인거냐? 라고할수도있는데, 이쯤되면박스에써놓을때보다는더기억에남지않겠는가? :) assertsame 은객체의주소를비교하는것이다보니, 주로동일객체임을증명하는데쓰인다. 이를테면캐시 (cache) 기능을만들었는데, 해당캐시가제대로동작하는지판단할필요가있다고가정해보자. 이럴때에 assertsame 을사용해서특정객체가캐시에서가져온객체와동일한지여부를판단할수있다. cache.add(someobject, KEY); assertsame(" 캐시처리실패!', someobject, cache.lookup(key)); 비슷한방식으로, 싱글톤 (Singleton) 5 으로만들어진객체를비교할때쓰이기도한다. assertsame 의실제구현은다음과같다. 5 싱글톤 (Singleton): 디자인패턴에서나온개념으로, 특정클래스의인스턴스가오직하나만생성될수있게만들어주는패턴이다. 이때 static 으로지정된 getinstance() 같은메소드를통해서만객체에접근가능하게만든다. 따라서몇번을호출해도동일한객체가지속적으로반환되거나이용된다. 일반적으로객체생성과소멸에비용이많은드는객체를싱글톤으로만들어놓아효율을높인다. 104 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
static public void assertsame(string message, Object expected, Object actual) { if (expected == actual) return; failnotsame(message, expected, actual); asserttrue([message], expected) assertfalse([message], expected) 예상값의참 / 거짓을판별하는단정문이다. boolean 타입메소드를이용할경우나, 부등호비교, 범위비교등을판단할때사용한다. 가끔 asserttrue(account. getbalance() == 0) 같은식으로등가비교를하는경우가있는데, 이때단정문이실패할경우 account.getbalance() 의값을바로알수없어불편하다. 등호비교는가급적 assertequals(0, account.getbalance()) 형식을사용하도록권장한다. assertnull([message], expected) assertnotnull([message], expected) 대상값의 null 여부를판단하는단정문이다. 자, 아래테스트메소드는성공일까, 실패일까? public void testaccount(){ account = null; assertnull(account); assertnull(account) 가성공해서녹색불이들어오는경우는, account 가 null 일때이다. 따라서답은성공, 녹색램프다! 별건아니지만, 처음사용할때자칫헷갈릴수있으므로유의하자. assert 시리즈는예상값이 assert 문장다음에이어지는글자와상태가일치한다는걸확인하는문장이다. assert 를 ~ 이어야함! 으로해석하면헷갈리지않을수있다. 2 장 JUnit 과 Hamcrest 105
fail([message]) 이메소드가호출되면해당테스트케이스는그즉시실패한다. 앞에서테스트케이스의성공 / 실패조건에서도설명했지만, 현재작성중인메소드의경우단정문을쓰지않았으면예외가발생하지않는이상무조건성공하는테스트케이스가된다. public void testaccumulatedcharge()throws Exception { Charge charge = new Charge(this.year); chage.add(additionalfee); // 이하계산로직미작성상태 위와같이단정문작성을누락한경우테스트를수행하면녹색막대가나온다. 이렇게되면자칫수많은테스트케이스속에슬쩍묻혀서테스트가성공적으로수행됐다고오해하고넘어갈수있다. 만일아직테스트케이스를작성중인데완료하지못한채구현을중단해야하는경우라면끝부분에 fail() 을추가해놓으면도움이된다. 나중에다시돌아와야할때어디부터해야할지를알려주는잣대가되기때문이다. 필자는이방식을 TDD 기본가이드중하나로삼고있다. JUnit 3에서는위와같은경우말고도, 예외처리를테스트하기위한트릭으로 fail() 을사용하기도한다. public void testwithdraw_ 현재잔고이하 _ 인출요구시 () throws Exception { Account account = new Account(10000); account.withdraw(20000); // OverWithdrawRequestException이발생해야함! 이를테면위와같은경우처럼특정조건에서예외가발생해야정상인경우를테스트케이스로작성하려면어떻게해야할까? 해답은다음과같다. public void testwithdraw_ 현재잔고이하 _ 인출요구시 () throws Exception { Account account = new Account(10000); try { account.withdraw(20000); // ➊ fail(); // ➋ 106 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
catch (OverWithdrawRequestException e){ asserttrue(true); // ➌ ➊ OverWithdrawRequestException 이발생해야함! ➋ 만일위에서예외가발생하지않아서이부분까지실행되면실패함 ➌ 빈줄로남겨둬도무방하나명시적으로표시해놓음 결과적으로 try 구문에서예외가발생하지않으면실패로간주하게된다. 지금까지 JUnit 의기본적인단정문들을살펴보았다. 자, 단정문을몇개정도기억하는지테스트해보고넘어가자. 오른쪽칸에단정문메소드이름을적어보자. 설명 이름 두값이같은지비교하는단정문은? 두객체가동일한객체인지비교하는단정문은? 예상값의참 / 거짓을판별하는단정문은? 대상값이 null이면참이되는단정문은? 호출즉시테스트케이스를실패로판정하는단정문은? JUnit 3 구성요소 3 테스트러너 Test runner - junit.swingui.testrunner.run( 테스트클래스.class); - junit.textui.testrunner.run( 테스트클래스.class); - junit.awtui.testrunner.run( 테스트클래스.class); 대부분의 Java 통합개발환경 (IDE) 은 JUnit 프레임워크를내장지원하고있다. 그래서종종 JUnit 이독립적인프레임워크라기보다는하나의기능처럼느껴질수도있다. 하 2 장 JUnit 과 Hamcrest 107
지만 JUnit 프레임워크는엄연히독립적인소프트웨어이고, 애초부터그렇게만들어졌다. 때문에명령행프롬프트에서실행하거나셸스크립트등을이용해실행할수도있다. 이를위해 JUnit 은테스트러너라는테스트실행클래스를제공한다. 기본적으로세가지실행방법을제공하는데, Swing UI, 텍스트그리고 Java AWT UI이다. 다음은 DisplayTest 라는테스트클래스를세가지방식으로모두실행하는예제다. import junit.framework.testcase; public class DisplayTest extends TestCase { public void testgetstring() { assertequals("happy", Display.getString()); public static void main(string [] args){ junit.swingui.testrunner.run(displaytest.class); junit.textui.testrunner.run(displaytest.class); junit.awtui.testrunner.run(displaytest.class); 만일특정테스트러너를명령행 6 에서직접수행하려고할경우에는다음과같이실행 한다. java - CP junit.jar;. junit.textui.testrunner DisplayTest Test suite JUnit 3 구성요소 4 테스트스위트 - 여러개의테스트케이스를한꺼번에수행하고자할때 - 테스트스위트는테스트케이스와다른테스트스위트를포함시킬수있다. - 메소드는반드시 public static Test suite( ) 여야한다. - 테스트추가는 suite.addtestsuite( 테스트클래스.class) 형식을갖는다. 6 cmd 창혹은커맨드입력창이라고도부른다. 108 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
Class DisplaySuiteTest { public static void main(string [] args){ junit.swingui.testrunner.run(displaysuitetest.class); public static Test suite() { TestSuite suite = new TestSuite(); suite.addtestsuite(displaytest.class); // 테스트케이스추가 suite.addtestsuite(displayreceivertest.class); suite.addtest(anothersuitetest.suite()); // 다른테스트스위트를포함할 // 경우 return suite; 테스트스위트는여러개의테스트케이스를함께수행할때사용한다. 예전에는이런식으로테스트스위트를작성했으나현재는잘사용하지않는다. 테스트클래스가추가되거나변경될때마다소스를직접수정해야하기때문이다. IDE에서제공하는기능을이용하거나 Ant나 Maven 등의빌드도구를사용해다수의테스트를수행한다. Ant 를이용한방법은추후에따로다뤄보겠다. 다음은이클립스 IDE의기능을이용해, 여러개의테스트를한꺼번에실행시키는방법이다. 이클립스를이용해여러개의테스트를한꺼번에실행시키기이클립스의탐색창에서프로젝트를선택하거나 src를선택한다음마우스오른쪽버튼을이용해 JUnit Test 를실행하면, 그하위에위치하고있는테스트클래스들이모두실행된다. 2 장 JUnit 과 Hamcrest 109
3 개의테스트클래스. 총 17 개의테스트케이스가수행됐다. JUnit 3으로테스트케이스작성하기 1장에서 JUnit 4 버전으로만들었던 Account 클래스와 Account 에해당하는테스트클래스를 JUnit 3 기준으로다시작성해보자. 동일하게진행하면다소지겨울수있으니까이번엔 Account 클래스의껍데기에해당하는클래스를먼저만들고, 테스트케이스를만들어보는식으로진행할생각이다. 다음은컴파일에러가나지않는수준으로 Account 클래스의껍데기를만들어본모습이다. 110 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
package main; public class Account { public Account(int i) { public int getbalance() { return 0; public void withdraw(){ public void deposit(){ 껍데기가만들어졌으면, 그다음엔이클립스의패키지탐색창 (Package Explorer) 에서 Account.java 를선택하고마우스오른쪽버튼을눌러 JUnit Test Case 를선택한다. 2 장 JUnit 과 Hamcrest 111
JUnit 3 test 를선택하고, 클래스의이름과테스트대상이되는클래스를적절하게선택한다음 Next 버튼을누른다. 112 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
조금전에미리만들어놓은메소드들이보인다. 대상메소드선택창아래에있는 Create tasks for generated test methods( 자동생성된테스트메소드들에대해작업목록만들기 ) 를선택하면테스트코드껍데기가만들어진다음에 TODO 태그를달아준다. 이때만든 TODO 태그는이클립스의 Tasks 뷰에서작업목록처럼확인할수있다. 테스트케이스의대상이되는메소드들을선택한다음 Finish 버튼을누른다. JUnit 3 라이브러리가빌드패스안에없네요. 추가하실래요? 만일클래스패스내에 JUnit 라이브러리가없으면, 추가할수있도록팝업창이뜬다. OK를누르자. 자동생성된테스트클래스 2 장 JUnit 과 Hamcrest 113
앞의코드는자동으로생성된테스트케이스껍데기에 TODO 까지적어본모습이다. 지금은로직이간단해서 TODO 를적는것이크게와닿지않을수있지만, 여기저기왔다갔다하면서소스를작성해야할경우 TODO 표시를해놓으면나중에이클립스의 Tasks 뷰를통해정리해서볼수있어편하다. TODO 로지정해놓으면 Tasks 뷰에서확인할수있다. 자동생성된테스트케이스들을수행시켜보자. 테스트케이스가모두실패하고있다. 이제테스트메소드각각에들어있는 fail() 메소드를제거하며, 하나하나테스트가통과할수있도록메소드를작성해보자. 앞장의예제를 JUnit 3 버전으로고쳐본코드의최종모습은아래와같다. AccountTest 클래스를 JUnit 3 버전에맞추어변경한소스 import junit.framework.testcase; public class AccountTest extends TestCase { Account account; 114 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
protected void setup() throws Exception { account = new Account(10000); public void testgetbalance() { assertequals(10000, account.getbalance()); public void testdeposit() { account.deposit(1000); assertequals(11000, account.getbalance()); public void testwithdraw() { account.withdraw(1000); assertequals(9000, account.getbalance()); 보면알겠지만, TestCase를상속했다는점과 @ 로시작하는애노테이션이없다는점을제외하면작성방식자체는유사하다 ( 애노테이션에대해서는잠시뒤에나올 JUnit 4 부분에서설명할예정이니까, 잠시만기다리자. 정궁금하면살짝가서보고와도무방하다 ). 앞에서도이야기했지만, 이번처럼작성하려는클래스의외형을먼저만들고테스트케이스를작성하는방식은그다지권장하는방식은아니다. 다만기존에작성되어있는코드 7 나, 일부테스트케이스없이작성중인코드에대해 TDD를진행할때 IDE 의기능을이용하면좀더쉽고빠르게작성할수있다. 누누이말할테지만, 시작부터습관을잘들이는일이중요하다. 가능한한하나씩테스트케이스를작성해서제품코드를만들어가는방식으로개발하자. 7 보통이런코드를레거시코드 (legacy codes) 라고부른다. 이미시스템이구축되어있고, 상당량의코드가작성되어있는상태에서추가로작업하게될때, 기존에존재하는코드를레거시코드로통칭한다. 말그대로유산으로물려받은코드인셈이다 ( 비록원했던건아니었을지라도말이다 ). 2 장 JUnit 과 Hamcrest 115
이클립스템플릿기능을이용한테스트메소드만들기 이클립스템플릿기능을사용하면테스트메소드를좀더쉽게만들수있다. 소문자로 test라고친다음컨텐트어시스트 (content assist) 라불리는이클립스의자동완성기능 ( + ) 을이용하면된다. 위그림에도살짝보이는데, 만일 JUnit 4 버전의테스트메소드를만들고자할때는소문자대신, 대문자 T로시작하는 Test라고타이핑한다음자동완성기능을이용하면된다. JUnit 3 으로작성한테스트클래스의구조 그리고 JUnit 3 을이용한테스트는일반적으로다음과같은구조를갖는다. public class NetworkTest extends TestCase { private Connection connection; // ➋ // ➊ public void setup() throws Exception { connection = new Connection("127.0.0.1"); // ➌ public void testsendmessage() throws Exception { // ➍ public void teardown() throws Exception { connection.close(); // ➎ ➊ TestCase 를상속 (extends) 받는클래스를만든다. ➋ 테스트에사용할테스트픽스처를정의한다. ➌ setup 메소드를사용해픽스처의상태를초기화한다. 116 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
➍ 이름이 test 로시작하는테스트케이스를작성한다. ➎ teardown 테스트를마친다음픽스처를정리할코드를작성한다. JUnit 테스트클래스에는생성자가없다? 느꼈을지모르겠지만 JUnit 테스트클래스는자신의생성자 (constructor) 를만들지않는다. 만들지않은게아니라, Java 문법상암묵적으로기본생성자는생략가능해서표현하지않은것아니냐고물을수도있다. 그럼명시적으로만들어놓으면어떻게동작하는지살펴보자. 이를테면앞선 AccountTest 예제에서생성자를만들었다고가정해보자. public class AccountTest extends TestCase { Account account; public AccountTest(){ System.out.println("Constructor was called!"); 위와같은식으로생성자를만든다음 JUnit 테스트를한번실행해보자. 테스트케이스에해당하는메소드는이전과마찬가지로정상실행되고, 부가적으로콘솔에다음과같이출력이된다. Constructor was called! Constructor was called! Constructor was called! Constructor was called! 생성자가네번호출된것처럼보인다. 에엣? 뭐지? 생성자는한번만호출되는것아닌가? 왜이리많이호출되는거지? 그리고네번찍힌이유는또뭘까? AccountTest 의테스트메소드를세어보자. testaccount( ), testdeposit( ), testwithdraw( ), testgetbalance( ) 네개!! 자, 조금느낌이오는가? JUnit 프레임워크는 Java의리플렉션 (reflection) 8 을사용해서테스트메소드를실행할때마다테스트클래스를강제로인스턴스화한다. 왜그랬을까? 왜테스트메소 8 원단어의뜻은 거울등에비친영상, 또는 반사 의의미다. Java 에서는리플렉션이라는기능을통해인스턴스화된객체로부터원래클래스의구조를파악해내어동적으로조작하는것이가능하다. 마치기계를분해해서마음대로재구성하는것처럼말이다. 리플렉션을이용하면 private 메소드나필드까지도마음대로조작할수있다. 리플렉션을사용하면, 이런식으로 Java 의일반적인규칙들을무시할수도있기때문에, 보통한정적으로만사용할것을권장한다. 그리고시스템비용이매우많이드는기능이다. 2 장 JUnit 과 Hamcrest 117
드를실행할때마다객체를새로만들어내는것일까? ( 계속읽어나가기전에잠깐책을덮고생각해보자!) 좋은테스트케이스는기본적으로다른테스트케이스의수행이나수행결과에영향을받지않아야한다. 이것이테스트의기본원칙이다. 따라서각각의테스트케이스를독립적으로수행하기위해, 테스트메소드수행전에테스트클래스자체를리셋한다. 그런다음각각의테스트메소드만수행하는식으로다른테스트메소드들로인한영향을최소화한다. 좀더자세한동작방식이궁금하다면 JUnit 소스를직접살펴보길아주약하게권한다. 왜냐하면우리의시간은소중하니까. 이것말고도알아야할게무궁무진하다. :) 2.1.2 JUnit 4 JUnit 4의특징 1. Java 5 애노테이션지원 2. test라는글자로 method 이름을시작해야한다는제약해소 Test 메소드는 @Test 를붙인다. 3. 좀더유연한픽스처 @BeforeClass, @AfterClass, @Before, @After 4. 예외테스트 @Test(expected=NumberFormatException.class) 5. 시간제한테스트 @Test(timeout=1000) 6. 테스트무시 @Ignore("this method isn't working yet") 7. 배열지원 assertarrayequals([message], expected, actual); 8. @RunWith( 클래스이름.class) JUnit Test 클래스를실행하기위한러너 (Runner) 를명시적으로지정한다. @RunWith 는 junit.runner.runner 를구현한외부클래스를인자로갖는다. 118 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
9. @SuiteClasses(Class[]) 보통여러개의테스트클래스를수행하기위해쓰인다. @RunWith 를이용해 Suite. class 를러너로사용한다. 10. 파라미터를이용한테스트 @RunWith(Parameterized.class) @Parameters public static Collection data() { 2002 년 JUnit 3.8 버전을이후로한동안새로운버전이없던 JUnit 이 2006 년에메이저버전업을하며다시돌아왔다. 기본적인실행철학은 3 버전과크게다르지않지만, 테스트케이스작성및실행을편리하게해주는다양한기능이추가됐다. JUnit 4에서의가장큰변경점을꼽으라면 Java 5에서부터지원되기시작한애노테이션 (annotation) 9 지원그리고 JUnit 4 버전대중반부터지원되기시작한비교표현을위한테스트매처라이브러리 (Test Matcher Library) 인 Hamcrest 도입, 이렇게크게두가지를꼽을수있겠다. Hamcrest 에대해서는조금뒤에보기로하고우선애노테이션도입에따른변경을살펴보기로한다. 애노테이션 JDK의히스토리를이야기할수있을정도의연륜있는개발자라면, JDK의여러버전중에서특별히구분해선을그을만한부분을기억할것이다. 크게두버전정도로나누어이야기를하곤하는데, 보통첫번째로 JDK 1.2 를꼽는다. 버전으로는 1.1 에서 1.2 로 0.1 올라섰지만, 개발자입장에서는변화의폭이상당히커서 1.2 버전은공공연히 Java 2라불렸다. 그다음으로 JDK 1.5 를많이뽑는데, 이버전은공식적으로 Java 5 10 9 마치주석처럼 @ 기호와함께선언적인형태로코드에달려있는문장. 10 이후썬 (Sun) 은 Java 5, Java 6 같은식으로한자리숫자로 JDK 버전을부르기로결정한다. 하지만다운로드받는파일은여전히 1.5, 1.6, 1.7 형태로되어있는데, 이후버전이올라가서 2.0이되면 Java 10이라고부를생각인건지궁금하다. 2 장 JUnit 과 Hamcrest 119
라불린다. API 추가 / 변경정도로버전업을하다가 1.5 에이르게되면서애노테이션, 제네릭스 (generics, c++ 의템플릿과유사 ), 향상된 for 루프문, 타입세이프한열거형타입 (type safe enumerations, Enum) 등새로운개념의요소가많이추가됐다. 11 특히애노테이션과제네릭스는개발자들사이에서도찬반양론말이많았다. 그도그럴것이그이전버전만개발한개발자들에게 1.5 버전에서추가된기능과키워드, 문법등으로개발된소스를보여주면, 한참멍하니쳐다보게된다. 그러나그건 Java 5가처음나왔던 2004 년시절이야기여야한다. 최근에새로배우는사람들은 Java 5의문법이나기능을원래 Java 가그런건가보다하고, 있는그대로받아들인다. 그렇기때문에버전업을거쳐왔던기존개발자와는달리 Java 5 기능들에대한거부감이확실히적다. 그리고거부감문제를떠나서새로추가된기능들은 Java 언어자체가좀더넓게확장하고다양하게발전할수있도록도와줬다. 특히그중하나가바로애노테이션이다. 애노테이션의가장큰장점하나는프레임워크의내부모델에대한자세한이해없이도각메소드의사용의도를명확하게문서화한다는점이다. 이를테면 JUnit 4의경우처럼 @Test 가붙어있으면, 척보기에도 아! 테스트대상메소드이구나! 라고직관적으로파악이가능하다. 물론, 이건단순한사례중하나일뿐이지만말이다. 근래에만들어지는대부분의 Java 애플리케이션은애노테이션을적극활용하는추세다. 한가지불편한점은 JUnit 3에서 JUnit 4로넘어가는분기점이애노테이션을사용할수있느냐없느냐로구분된다는점이다. 따라서 JUnit 도애노테이션기능을추가해서확장과발전을도모하게되는데, 그시작이 JUnit 4.0 이다. 물론 3.8 이후로수년간메이저버전업이없던사이에 TestNG 라는애노테이션기반의 xunit 테 11 만일아직 Java 5 의기능을확인해보지않았다면, 이번기회에꼭한번살펴볼것을권장한다. 근래에만들어지는대부분의프레임워크는 Java 5 이후의기능들을적극사용해안정성과가독성을높이는방향으로작성되고있다. 때문에언제가됐든알아야하는날이온다 ( 그러니이왕이면미루지말자 ). 참고로 Java 5 도이미서비스지원종료 (end of service life, EOSL) 로들어섰다. 이젠 Java 6, Java 7 등을봐야할시기이지만, 우선한단계씩차분히나아가자. 웹검색을통해여러사람이작성한해설내용을보는것도좋지만, 우선은썬의공식문서인 New Features and Enhancements J2SE 5.0 (http://java.sun.com/j2se/1.5.0/docs/relnotes/features.html) 을볼것을추천한다. 검색으로찾게되는단편적인지식이아닌, 좀더상세하고도광범위한정보를얻을수있다. 정시간이없다면, 간략화버전인 J2SE 5.0 in a Nutshell (http://java.sun.com/developer/technicalarticles/releases/j2se15/) 이라도봐두자. 120 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
스트프레임워크가주목을받았었다. TestNG 는 Retroweaver(http://retroweaver. sourceforge.net/) 라는라이브러리를이용해 JDK 1.4 이하에서도애노테이션을쓸수있게해줬다. 12 애노테이션적용에있어 TestNG 보다후발주자였던 JUnit 4는의도했든아니든 TestNG 의상당기능을그대로차용해왔고, 그로인해일부비난을받기도했다. 어쨌든 JUnit 4는애노테이션사용을특정라이브러리에의존하지않고순수 JDK 기능지원을이용하기로정했기때문에 JUnit 4를기동하기위해필요한최소 JDK 버전은 Java 5(JDK 1.5) 가됐다. 어찌보면별일아닐수있지만, 사용자입장에서는소위말하는 최소사양 이올라가버리게된상항이불만족스러울수도있다. 그래서예전에는 JDK 1.4 이하버전에서도다양한픽스처나부가기능을누리기위해서 JUnit 대신 TestNG 를테스트프레임워크로사용하는경우가종종있었다. 하지만흐르는강물은되돌릴수없다고했던가? 인지도측면에서 JUnit 이월등히높았고, 거의비슷한표현식에동일한기능까지제공하는 JUnit 4가나오면서, 애노테이션을이용한테스트케이스작성에도 JUnit 이주로사용되게됐다. 사실 JUnit 3과그이전버전에서공격받았던점중하나는, JUnit 이테스트클래스내에존재하는메소드의이름중에서 test 라는글자로시작하는메소드만을테스트메소드로인식해실행한다는점이었다. 애노테이션은오랫동안끌어왔던이런문제점을해결해줬다. 그뿐아니라, 애노테이션을사용해서더강력한테스트방식들을더간결히지원해줄수있게됐다. 예를들어확장기능을사용하기위해서라이브러리를호출하거나부모클래스로부터물려받지않고, 미리정의된애노테이션만호출하면끝나는식으로말이다. 그럼이제부터는 JUnit 4에서제공하는애노테이션들과이를통한추가기능에는어떤것들이있는지차근히살펴보자. 12 Retroweaver 라이브러리를사용하면애노테이션뿐아니라 Java 5 의새로운기능대부분을 1.4 에서사용할수있다. 심지어기본타입 (primitive type) 과 Wrapper 클래스를직접비교 / 할당가능하게만들어주는오토박싱 (autoboxing/ unboxing) 까지말이다. 2 장 JUnit 과 Hamcrest 121
애노테이션의기원, XDoclet Java 언어는 @ 기호와 Javadoc 유틸리티를이용해한정적으로문서를자동으로생성해주는기능을초창기부터제공했었다. 프로그래밍코드중간에 @ 기호를이용해일종의짤막한명세 (spec) 를작성하면, 그걸툴이알아서문서로만들어주는기능인데, 많은사람이편리해했다. 이런식의 Javadoc 기능을이용한 API 문서생성처럼, 프로그램코드내부에적혀있는선언적인주석기호 @ 을이용한작업방식에관심을기울였던사람들이있었다. 이들이만들어낸것이바로 XDoclet이라는소스코드생성엔진이다. XDoclet은 Javadoc의 Doclet 엔진을확장해서만든오픈소스프로젝트로, 개발자의작업량을줄여준다는점과속성기반프로그래밍 (attribute oriented programming) 이라는이름의개발방식으로한동안유명했었다. 간단히말하면 주석문장으로문서만만드냐? 소스코드도만들어내자! 이런취지였는데, 현재는초반열기와다르게이런저런이유로한정적으로만사용되는분위기다. 어쨌든 XDoclet 은속성기반프로그래밍이라는개발방식에대한인지를넓히는계기가됐고, 현재의애노테이션을이용한개발에많은영향을줬다. @Test 기존에는테스트케이스에해당하는메소드로지정하기위해서는메소드이름을소문자 test 로시작해야한다는규칙이있었다. 이런방식을흔히작명패턴 (naming pattern) 이라고하는데, JUnit 4에와서는메소드이름과관계없이 @Test 애노테이션만붙이면테스트메소드로인식한다. 즉, 더이상소문자 test 로메소드의이름을시작하지않아도된다는뜻이다. 하지만많은사람이여전히메소드이름을소문자 test 로시작하는방식을따르고있다. @Test public void testdeposit() throws Exception { account.deposit(1000); assertequals(11000, account.getbalance()); 개발자에따라서는 test 소문자로시작하던걸애노테이션붙여서 @Test 로사용하는게무슨장점이된거냐? 고물을수도있는데, 생각보다많은사람이 testdeposit 대신 122 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
에 tsetdepoist 으로타이핑하고는 왜실행이안될까? 고민한다. @Test 로지정해놓으면그럴일이없다. 13 테스트픽스처메소드추가지원 JUnit 3에서는 setup 과 teardown 이라는두개의테스트픽스처메소드를제공했는데, JUnit 4에서는각각을 @Before 와 @After 라는이름의애노테이션으로지원해준다. 그리고 JUnit 3에서는테스트클래스에서단한번만실행할수있게해주는방법을공식적으로제공하지않아다소불편했었는데, JUnit 4에서는 @BeforeClass, @AfterClass 두개의애노테이션을이용해하나의테스트클래스내에서한번만실행하는메소드를만들수있게됐다. 추가된테스트픽스처메소드와실행순서 13 @Test 애노테이션스펠링도틀릴순있진않느냐? @Tset 으로타이핑하는건왜배제하느냐? 고까탈스럽게물을수도있는데, 애노테이션을사용하면컴파일러가개발자의눈대신에체크를해주기때문에그런일은생기지않는다. 이와비슷한경우로수많은개발자가클래스를상속받아서오버라이드할때오타를내서의도하지않은결과를만들곤했다. Java 5 부터지원되는 @Override 애노테이션이바로그런상속시발생하는작명패턴 ( 부모와메소드이름과시그니처가같아야한다 ) 의오류를막아준다. 편리하고유용하니여러분도적극적으로사용해보면좋을것이다. 2 장 JUnit 과 Hamcrest 123
다음은터미널클라이언트프로그램에대한테스트클래스다. 네트워크연결은한번만하고, 각각의테스트케이스를로그인 테스트수행 로그오프순으로실행한다. 그리고테스트케이스를모두수행한다음엔네트워크접속을종료한다. public class TerminalTest { private static Terminal term; @BeforeClass public static void setupbeforeclass() throws Exception { term = new Terminal(); term.netconnect(); // 터미널에접속한다. @AfterClass public static void teardownafterclass() throws Exception { term.netdisconnect(); // 터미널과의연결을해제한다. @Before public void setup() throws Exception { term.logon("guest", "guest"); // 시스템에로그온 @After public void teardown() throws Exception { term.logoff(); // 시스템으로부터로그오프 @Test // 정상적으로로그인됐는지테스트 public void testterminalconnected() throws Exception { asserttrue( term.islogon() ); System.out.println("== logon test"); @Test // 터미널메시지테스트 124 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
public void testgetreturnmessage() throws Exception { term.sendmessage("hello"); assertequals("world!", term.getreturnmessage()); System.out.println("== message test"); 실행결과는다음과같다. Network is estabilished. >>logon guest:guest == logon test <<logoff >>logon guest:guest == message test <<logoff Network is disconnected. setupbeforeclass() setup() testterminalconnected() teardown() setup() testgetreturnmessage() teardown() teardownafterclass() 예외테스트 JUnit 3에서는예외를테스트하는공식적인방법을제공하지않았다. 대신에 try/ catch 문과 assert 단정문을일종의트릭처럼사용해서테스트를진행하곤했다. 다음은 JUnit 3에서예외를테스트하는코드다. public void testexception(){ String value = "a108"; try { System.out.println(Integer.parseInt(value)); asserttrue(false); catch (NumberFormatException nfe){ asserttrue(true); 2 장 JUnit 과 Hamcrest 125
Junit 3에서는예외처리를테스트하려면위와같이 try/catch 문을사용해야만했다. 다음은동일한코드를 JUnit 4의애노테이션을이용해작성한코드다. @Test (expected=numberformatexception.class) public void testexception(){ String value = "a108"; System.out.println(Integer.parseInt(value)); JUnit 4에서는 @Test 애노테이션의속성중 expected 를이용해예외처리를테스트한다. expected 의값으로예외클래스를지정했을때, 만일테스트메소드내에서해당예외가발생하지않는다면테스트메소드를실패로간주한다. 테스트시간제한 @Test(timeout=5000) public void testpingback() throws Exception { ciserver.sendnotimail(); assertequals(notificated, ciserver.getstate()); @Test 의속성으로 timeout 을지정하고, 그값으로밀리초단위의시간을정해준후해당시간내에테스트메소드가수행완료되지않으면실패한테스트케이스로간주한다. 테스트무시 @Ignore @Test public void testterminal() throws Exception { asserttrue(term.islogon()); System.out.println("== logon test"); @Ignore 가붙은테스트메소드는해당애노테이션을지우기전까진수행하지않는다. 126 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
배열지원 @Test public void testarrayassertequals() throws Exception { String [] names = {"Tom", "JIMMY", "JOHIN"; String [] anothernames = {"Tom", "JIMMY", "JOHIN"; assertarrayequals(names, anothernames); JUnit 3에서는지원되지않았던기능으로, 배열을비교할수있게해준다. 아쉽지만배열원소의자리순서기준으로 equals 비교가이뤄지기때문에비록배열안의값집합은동일하더라도순서가다르면테스트가실패한다는점을유의하자. 다음은해당예를보여준다. @Test public void testarrayassertequals() throws Exception { String [] names = {"Tom", "JIMMY", "JOHIN"; String [] anothernames = {"Tom", "JOHIN", "JIMMY"; assertarrayequals(names, anothernames); 배열안의순서가다르면실패한다. 2 장 JUnit 과 Hamcrest 127
자동완성기능을이용해 static 클래스의메소드사용하기 JUnit 4는 TestCase 상속대신에애노테이션을사용하기때문에 assertequals 같은기본메소드조차 static import로지정하지않으면쓸수가없다. 자동완성기능도제대로동작하지않을때가많은데, 이클립스가모든 static 클래스에대한메소드를미리인지하고있을수없기때문이다. 결국개발자가직접 static import 구문을타이핑하거나, 전체클래스이름 (full quilified name) 을적어줘야하는불편함이발생한다. 이를테면 JUnit 4의 matcher 메소드중하나인 containsstring 이라는메소드를호출하기위해서는 static import org.junit. matchers.junitmatchers.*; 와같은 import 구문을직접타이핑하거나 org.junit.matchers. JUnitMatchers.containsString( ~~ ); 식으로호출해야한다. 이런불편함을해결하는방법은이클립스자동완성 (Content Assist) 의 Favorites 기능을이용하는것이다. 이클립스설정 (Preferences) 의필터창에 static이라고타이핑한다음 Editor Content Assist Favorites 를선택한다. 그다음 New Type 을선택해서자주사용하는 static 클래스를등록한다. 128 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
등록된 static 클래스가표시된다. 이제에디터창에서자동완성기능을이용해쉽게 static 클래스의메소드들을사용할수있다. @RunWith JUnit 프레임워크에서테스트클래스내에존재하는각각의테스트메소드실행을담당하고있는클래스를테스트러너 (Test Runner) 라고한다. 이테스트러너는테스트클래스의구조에맞게테스트메소드들을실행하고결과를표시하는역할을수행한다. 우리눈에는보이지않지만, 테스트케이스를이클립스에서실행하면내부적으로는 JUnit 의 BlockJUnit4ClassRunner 라는테스트러너클래스가실행되고, 이클립스는그결과를해석해서우리에게보기편한화면으로보여준다. @RunWith 애노테이션은 JUnit 에내장된기본테스트러너인 BlockJUnit4ClassRunner 대신에 @RunWith 로지정된클래스를이용해클래스내의테스트메소드들을수행하도록지정해주는애노테이션이다. 일종의 JUnit 프레임워크의확장지점인셈이다. 사실, 사용하는입장에서는이렇게까지자세히알필요는없다. 다만, 이런구조를이용해서많은애플리케이션이나프레임워크가자신에게필요한테스트러너를직접만들어자신만의고유한기능을추가해테스트를수행하고있다는것정도만알아두자. 이를테면스프링프레임워크 2 장 JUnit 과 Hamcrest 129
(Spring Framework) 14 에서제공하는 SpringJUnit4ClassRunner 같은클래스는이확장기능을이용한대표적인사례중하나다. @RunWith 로 SpringJUnit4ClassRunner. class 를지정하면 @Repeat, @ProfileValueSourceConfiguration, @IfProfileValue 등스프링에서자체적으로만들어놓은추가적인테스트기능을이용할수있게된다. 그리고굳이외부가아니더라도 JUnit 내부에도기본러너를확장한클래스가몇개있는데, 그중하나가 SuiteClasses 이다. @SuiteClasses Suite 클래스는 JUnit 3에서의 static Test Suite() 메소드와동일한일을수행한다. 즉, @SuiteClasses 애노테이션을이용하면여러개의테스트클래스를일괄적으로수행할수있다. 다음예제는 ATest, BTest, CTest 세개의테스트클래스를순차적으로실행하고싶을때 JUnit 3과 JUnit 4에서는각각어떻게작성해야하는지를보여준다. JUnit 4의경우 @RunWith(Suite.class) @SuiteClasses(ATest.class, BTest.class, CTest.class) public class ABCSuite { JUnit 3의경우 public class ABCSuite extends TestCase { public static Test suite() { TestSuite suite = new TestSuite(); suite.addtestsuite(atest.class); suite.addtestsuite(btest.class); suite.addtestsuite(ctest.class); 14 객체간에서발생하는의존성처리를컨테이너가처리하도록만든의존관계주입컨테이너 (Dependency Injection Container) 와관점지향프로그래밍 (Aspect-Oriented Programming) 기법활용등으로유명한자바프레임워크. 각각에대해좀더알고싶으면위키피디아를참고하면많은도움이된다. 130 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
return suite; JUnit 4에는이외에도흥미로운기능들이몇개더추가되어있다. 해당내용은이책의부록 A.1 절 놓치기엔아까운 JUnit 4의고급기능들 에서설명하고있다. 중요하지않아서라기보다는중급이상의개발자들에게조차다소어렵고, 도전적인내용들이기에위치를옮겼다. 힘은들겠지만, 익혀두면확실히도움이될내용들이포함되어있으니언제가되었든꼭살펴보도록하자. 대신여기서는어떤내용이있는지정도만간략히소개할까한다. 2장시작부분의체크리스트에표시해놓고기회가될때부록에수록된내용을살펴보자. 파라미터화된테스트 (Parameterized Test) 하나의메소드에대해다양한테스트값을한꺼번에실행시키고자할때사용한다. 이를테면다음과같은과세표준전구간을테스트하려한다면어떻게테스트시나리오를작성하고실행할것인가가이슈가된다. 2008년과세표준 세율 누진공제액 1200만원이하 8% 0원 1천2백만원초과 ~4천6백만원이하 17% 108만원 4천6백만원초과 ~8천8백만원이하 26% 522만원 8천8백만원초과 35% 1,314만원 테스트메소드가많아지거나구문이장황해질수있는데, 파라미터화된테스트를사용하면테스트케이스를효율적으로작성할수있다. 2 장 JUnit 과 Hamcrest 131
룰 (Rule) JUnit 4.7 버전부터추가된기능으로하나의테스트클래스내에서각테스트메소드의동작방식을재정의하거나추가하기위해사용하는기능이다. 테스트케이스수행을좀더세밀하게조작할수있게된다. 룰의종류는다음과같다. 규칙이름 설명 TemporaryFolder 임시폴더 테스트메소드내에서만사용가능한임시폴더나임시파일을만들어준다. ExternalResource 외부자원 외부자원을명시적으로초기화한다. ErrorCollector 에러수집기 테스트실패에도테스트를중단하지않고진행할수있게도와준다. Verifier 검증자 테스트케이스와는별개의조건을만들어서확인할때사용한다. TestWatchman 테스트감시자 테스트실행중간에사용자가끼어들수있게도와준다. TestName 테스트이름 테스트메소드의이름을알려준다. Timeout 타임아웃 일괄적인타임아웃을설정한다. ExpectedException 예상된예외 테스트케이스내에서예외와예외메시지를직접확인할때사용한다. 이론 (Theory) 테스트데이터와상관없이작성대상메소드를항상유지해야하는논리적인규칙을표현할때사용한다. 아직은실험적인성격이강한기능이다. 작성된코드의모습이어떠한지정도만살펴보고, 흥미가간다면마찬가지로부록을참조하기바란다. 외국개발자들조차도많이어려워하는내용이다. @RunWith(Theories.class) public class SquareRootTheoryTest { @DataPoint public static double FOUR = 4.0; @DataPoint public static double ZERO = 3.0; @Theory public void defineofsquareroot(double n){ assertequals(n, sqrroot(n)*sqrroot(n), 0.01); 132 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
asserttrue( sqrroot(n) >= 0 ); 정수 n 은 n * n 과같다는이론을정의해놓고있다. 테스트케이스의데이터값뿐아니 라, 이공식도만족시켜야테스트가정상통과한다. JUnit 버전대별패키지크기와파일개수 ( 예제샘플패키지제외 ) JUnit 1 패키지수 3 개, Java 파일수 14 개, 테스트파일포함되어있지않음 JUnit 2(2.1 기준 ) 패키지수 5 개, Java 파일수 21 개, 테스트파일 11 개 JUnit 3(3.8 기준 ) 패키지수 10 개, Java 파일수 49 개, 테스트파일 37 개 2 장 JUnit 과 Hamcrest 133
JUnit 4(4.6 기준 ) 패키지 29 개, Java 파일 137 개, 테스트 36 개 그리고 JUnit 4.4 이상버전부터는 Hamcrest 라이브러리를의존패키지로갖는다. 보면알겠지만 JUnit 4가한때비난을받았던이유중하나가바로클래스의증가로인한복잡도증가였다. 다양한요구사항과변화하는개발환경에대응하기위해서는어쩔수없는선택이었겠지만, 원저작자였던켄트벡도언급했듯이프레임워크자체의복잡도증가는다소아쉬운점이다. 2.2 비교표현의확장 : Hamcrest Hamcrest( 햄크레스트 ) 는 jmock 이라는 Mock 라이브러리저자들이참여해만들고있는 Matcher 라이브러리 15 로, 테스트표현식을작성할때좀더문맥적으로자연스럽 15 Matcher 라이브러리 : 필터나검색등을위해값을비교할때좀더편리하게사용할수있게도와주는라이브러리. 앞에서 JUnit 라이브러리를이클립스의빌드경로에추가할때함께뒤따라붙었다고이야기했던 org.hamcrest. core_1.1.0 의정체가바로이것이다. 134 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
고, 우아한문장을만들수있게도와준다. Matcher 는이름그대로어떤값들의상호일치여부나특정한규칙준수여부등을판별하기위해만들어진메소드나객체를지칭한다. 지칭하는사람에따라서는언어자체에서지원하는기본적인비교표현식인 ==, >, >=, <= 에서부터 equals 나 contains 처럼대조를위해만드는메소드까지, 조건일치여부를알려줄수있다면어떠한것이든 Matcher 의범주에포함시키기도한다. 흔히데이터필터링 (data filtering) 이나 UI 유효성검증 (validation) 등의기능을구현할때 Matcher 관련기능이많이사용된다. Hamcrest 는다양한 Matcher 들이모인, Macher 집합체다. 초기에 jmock 라이브러리의일부로포함되어들어있던 비교표현 API 들에불과했으나, 그유용성을인정받아리팩토링을통해 jmock 으로부터독립했다. 물론현재는엄연한하나의 Matcher 라이브러리프로젝트로서독자적으로발전해나가고있다. Hamcrest 라이브러리를단위테스트프레임워크와함께사용하면테스트케이스작성시에문맥적으로좀더자연스러운문장을만들어준다. 좀더생활언어에가까운테스트케이스구문이만들어지고, 소스를보는사람이편하게읽을수있게되며, 테스트케이스작성자의의도가더욱더명확하게드러나게된다. 이런이유로시간이지나면서점차많은사람이테스트케이스작성시에 Hamcrest Matcher 라이브러리를사용하는추세다. 예전에는 JUnit 과같은테스트프레임워크를사용할때, 곁들여함께사용하는라이브러리였는데, 인기덕분인지 JUnit 버전 4.4 부터는아예 JUnit 배포라이브러리안에포함되어함께배포된다. 앞으로더많은프레임워크나라이브러리, 혹은프로그램언어들이 Hamcrest 라이브러리가추구하는방식을따르게되리라예상된다. 개발자만이읽을수있는프로그래밍언어라는느낌보다는좀더문장체에가까운느낌으로넓은범주의사람들이함께이해할수있는형태로만들어주기때문이다. 현재 Hamcrest 는 Java 외에도 C++, Objective-C, Python 그리고 PHP 버전으로도포팅되어있다. 2 장 JUnit 과 Hamcrest 135
Hamcrest Project 사이트, http://code.google.com/p/hamcrest/ Hamcrest 라이브러리는기본적으로 assertequals 대신에 assertthat 이라는구문사용을권장한다. 공학적인느낌을주는딱딱한 assertequals 보다는 assertthat 이좀더문맥적인흐름을만들어준다고여기기때문이다. assertequals( "YoungJim", customer.getname() ); assertthat( customer.getname(), is("youngjim") ); 두문장의차이점이느껴지는가? 어쩌면그다지별반다르지않다고느낄수도있다. 하지만조금만더관심을갖고살펴보면문장의흐름이라는측면에서는아래쪽구분이더자연스럽다는걸알수있다. 16 이렇듯자연스런문장을통해서테스트비교구문을만드는것이 Hamcrest 의사용목적이다. 16 모국어가영어가아닌지라크게와닿진않을수있다. 136 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
자, 그럼 assertthat 의일반적인사용방법은어떤지한번살펴보자. assertthat( 테스트대상, Matcher 구문 ); assertthat(" 메시지 ", 테스트대상, Matcher 구문 ); assertequals 와마찬가지로메시지는선택사항이다. 이제추가적으로간단한몇가지예제문장을더살펴보자. Hamcrest 적용 적용전 assertequals(100, account.getbalance( )); 적용후 assertthat(account.getbalance( ), is(equalto(10000))); 적용전 assertnotnull(resource.newconnection( )); 적용후 assertthat(resource.newconnection( ), is(notnullvalue( )); 적용전 asserttrue(account.getbalance( ) > 0); 적용후 assertthat(account.getbalance( ), isgreaterthan(0)); 적용전 asserttrue(user.getloginname().indexof ( Guest ) > -1); 적용후 assertthat(user.getloginname(), containsstring( Guest )); 위예제는일반적인 JUnit 의 assertequals 계열을 Hamcrest 의 assertthat 계열로바꾸면어떻게되는지를보여주고있다. 잠시뒤에좀더자세히설명하겠지만 is, equalto, greaterthan 등의메소드가 Matcher 구문에해당한다. Matcher 구문 ( 메소드 ) 은기본적으로 static 으로선언되어있고, 리턴값은 Matcher 클래스로되어있다. 프로그래밍문장자체만보면, Hamcrest 사용유무여부가큰차이가없어보이지만, 전반적으로좀더읽기편한자연어 (natural language) 17 에가깝게바뀐다. import static org.junit.assert.*; import static org.hamcrest.corematchers.*; 17 프로그래밍언어가아닌일상언어나문장체를지칭한다. 2 장 JUnit 과 Hamcrest 137
import org.junit.test; public class HamcrestTest { @Test public void testarrary() throws Exception { assertthat("start Date 비교 ", "2010/02/03", is("2010/02/04")); 위소스는 JUnit 4에포함된 Hamcrest 라이브러리를사용한소스다. assertthat 은 org.junit.assert 클래스내에존재하고, is를비롯한 Matcher 들은 org.hamcrest. CoreMatchers 클래스를통해호출한다. 앞에서 JUnit 4에대해설명할때도이야기했지만, 이클립스자동완성기능의도움을바로받을수없기때문에, 위와같이 static import 로사용할클래스를직접적어주거나사전에 Favorite 기능으로추가해놓았어야한다. 정리해서말하면, JUnit 4에서 assertthat 을사용하려면위소스의첫두줄에해당하는부분이만드시 static import 되어있어야한다는이야기다. 실행결과 Java.lang.AssertionError: Expected: is "2010/02/04" got: "2010/02/03" Hamcrest 라이브러리를사용했을때의기본실패메시지는위와같다. 예상은 무엇 인데, 실제로는 무엇 이나왔음 의형태다. 좀더문맥적흐름이자연스러워서받아들이기더편하다 ( 물론영어실력이뒷받침되어있어야한다. 에휴 ). JUnit 4 내부적인이야기를조금더하자면, JUnit 4는 Hamcrest 라이브러리를자연스럽게사용하기위해 Hamcrest 의 assertthat 을 org.junit.assert 클래스에서일부다시구현하고있다. 그래서 JUnit 사용자입장에서는 Hamcrest 라이브러리의존재유무를알지못해도 assertthat 메소드를자연스럽게사용할수있게했다. 물론 is 같은 Hamcrest 고유 Matcher 메소드들은 Hamcrest 쪽을라이브러리를사용하는걸로놔두고있다. 138 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
클래스패스내에존재하지만인식안되는 Hamcrest 라이브러리? 경우에따라서는최신 Hamcrest 라이브러리를다운로드받아서클래스패스내에갖다놓더라도정상적으로동작하지않을때가있다. 왜냐하면 JUnit 라이브러리내에도 Hamcrest 라이브러리가이미들어있기때문이다. 개똥참외도먼저맡은놈이임자라고했던가? JUnit 라이브러리쪽에포함된 Hamcrest 의클래스들이먼저인식되어있기때문에, 나중에추가된라이브러리를인식하지못하는것이다. 이런경우가가끔씩발생하기때문에, 이클립스에서외부모듈을갖다쓸경우에는해당문제가발생하지않도록다소주의를기울일필요가있다. 만일클래스패스내에 Hamcrest 라이브러리가존재함에도특정클래스나메소드를인식하지못한다면, 클래스패스우선순위를따져보자. 의심스러울경우원하는클래스의로딩이먼저일어날수있도록우선순위를강제로변경시켜준다. 이클립스에서는프로젝트메뉴의 Properties 탭의 Java Build Path에서우선순위를임의로조정해준다. 아래그림은 Hamcrest 라이브러리를 JUnit 라이브러리보다상위로올려놓은모습이다. 사실이런식의 PATH 우선순위관련문제들이단지 Java 클래스패스경우뿐아니라, 일반적인 PATH 관련부분에서종종발생한다. 이를테면, 윈도우용오라클을설치하면실행패스내에서오라클설치시에사용되는 Java 런타임 1.4 버전이떡! 하니시스템패스맨앞으로잡히게된다. 누가이런걸예상했겠는가? 그냥, 어느날어느순간엔가부터는자신의시스템기본 Java 런타임환경이 1.4로바뀌는원인불규명상황에빠지게돼버릴뿐이다. 그러니, 컴퓨터의세계에는심령술과동급레벨인미지의영역이한자리차지하고앉아있는것이다. 지금도그러는지모르겠지만, 예전엔전산실앞에서돼지머리놓고시스템장애가발생하지않게해달라고고사를 2 장 JUnit 과 Hamcrest 139
지내는모습도몇번봤다. 그당시엔그게참어이없고아이러니했는데, 지나고보니꼭그렇지만은않더라. 이야기가너무나갔다고생각하지만, 몇마디덧붙이자면필자는컴퓨터가거짓말을안한다고생각한다. 모든일에는이유가있지만, 밝혀내지못하는것뿐이라고말이다. 하지만때때로이런생각이들곤한다. 컴퓨터는거짓말은안하지만, 종종시침뚝떼고눈멀뚱멀뚱한모습으로사람을속이긴한다고말이다. Hamcrest 라이브러리를한번자세히살펴보자. JUnit 4.7 이나 4.8 에기본으로탑재되어있는 Hamcrest 버전은 1.1 core 버전이다. Hamcrest 가제공하는클래스와메소드의수가적지않다보니, 버전별로그리고패키지별로따로제공하고있다. 여기서는여러패키지를포함하고있는 1.2 all 버전을기준으로, 많이사용하는 Matcher 위주로살펴볼예정이다. 따라서현재이클립스에기본으로탑재되어있는 JUnit 4.5 버전안에들어있는 Hamcrest 와는조금차이가날수있다. 하지만차기이클립스에는 JUnit 상위버전과함께 Hamcrest 도상위버전이탑재될예정이라고하니, 미리살펴본다고생각하자. 그리고 Hamcrest 1.2 버전에좀더다양한기능이추가되어있기때문에, 이왕사용할거면 1.2 를내려받아서사용할것을권장한다. 필요에따라 Hamcrest 사이트에서해당버전을내려받아서클래스패스내에추가해서사용하면되겠다. Hamcrest 라이브러리는기본적으로 core 패키지와그외의확장패키지로구성되어있다. 패키지 org.hamcrest.core org.hamcrest.beans org.hamcrest.collection org.hamcrest.number org.hamcrest.object org.hamcrest.text org.hamcrest.xml 설명오브젝트나값들에대한기본적인 Matcher들 Java 빈 (Bean) 과그값비교에사용되는 Matcher들배열과컬렉션 Matcher들수비교를하기위한 Matcher들오브젝트와클래스들비교하는 Matcher들문자열비교 XML 문서비교 140 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
다음은분류에따른 Matcher 의종류를좀더자세히표시해봤다. 단, 패키지기준으로분류한게아니라 Matcher 의종류에따라분류해놓았기때문에, 분류가곧패키지와동일하진않다는점에유의하자. 이를테면 allof 같은경우엔논리적비교에가깝지만, 실제들어있는것은 core 패키지내에포함되어있다. 패키지위치보다는어떤종류의 Matcher 를사용할수있는지중심으로살펴보자. 대부분은클래스를직접 import 하지않고 org.hamcrest.corematchers 나 org.hamcrest.matchers 클래스만 static import 처리하면호출할수있도록되어있다. 또한여기소개된것외에도추가로제공되는 Matcher 도있지만, 원저작자가밝힌자주사용되는대표적인 Matcher 위주로소개한다. 코어 (Core) 메소드 설명 클래스명 anything 어떤오브젝트가사용되든일치한다고판별한다. IsAnything describedas 테스트실패시에보여줄추가적인메시지를만들어주는 DescribedAs 메시지데코레이터 equalto 두오브젝트가동일한지판별한다. IsEqual is 내부적으로 equalto와동일하다. 가독성증진용. Is 즉, 아래세문장은의미가동일하다. assertthat(entity, equalto(expectedentity)); assertthat(entity, is(equalto(expectedentity))); assertthat( entity, is(expectedentity) ); 오브젝트 (Object) 메소드 설명 클래스명 hastostring tostring 메소드의값과일치여부를판별한다. HasToString assertthat(account, hastostring( Account )); instanceof 동일인스턴스인지타입비교 (instance of). IsInstanceOf typecompatiblewith 동일하거나상위클래스, 인터페이스인지판별 IsCompatibleType notnullvalue Null인지, 아닌지를판별 IsNull nullvalue sameinstance Object가완전히동일한지비교. IsSame equals 비교가아닌 ==( 주소비교 ) 로비교하는것과동일 2 장 JUnit 과 Hamcrest 141
논리 (Logical) 메소드 설명 클래스명 allof 비교하는두오브젝트가각각여러개의다른오브젝트를포함하고있을경우에, AllOf 이를테면 collection 같은오브젝트일경우서로동일한지판별한다. Java의숏서킷 (&& 비교 ) 과마찬가지로한부분이라도다른부분이나오면그순간 false를돌려준다. anyof allof와비슷하나 anyof는하나라도일치하는것이나오면 true로판단한다. AnyOf Java의숏서킷 ( ) 과마찬가지로한번이라도일치하면 true를돌려준다. not 서로같지않아야한다. IsNot 빈즈 (Beans) 메소드설명클래스명 hasproperty Java 빈즈프로퍼티테스트 HasProperty 컬렉션 (Collection) 메소드 설명 클래스명 arrary 두배열내의요소가모두일치하는지판별 IsArray hasentry, haskey, hasvalue 맵 (Map) 요소에대한포함여부판단 IsMapContaining hasitem, hasitems 특정요소들을포함하고있는지여부판단 IsCollectionContaining hasiteminarray 배열내에찾는대상이들어있는지여부를판별 IsArrayContaining 숫자 (Number) 메소드 설명 클래스명 closeto 부동소수점 (floating point) 값에대한근사값내일치여부, IsCloseTo 값 (value) 과오차 (delta) 를인자로갖는다. greaterthan 값비교. >, >= OrderingComparison greaterthanorequalto lessthan 값비교. <, <= OrderingComparison lessthanequalto 142 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
텍스트 (Text) 메소드 설명 클래스명 containsstring 문자열이포함되어있는지여부 StringContains startswith 특정문자열로시작 StringStartsWith endswith 특정문자열로종료 StringEndsWith equaltoignoringcase 대소문자구분하지않고문자열비교 IsEqualIgnoringCase equaltoignoringwhitespace 문자열사이의공백여부를구분하지않고비교 IsEqualIgnoringWhiteSpace 보통 JUnit 같은 xunit 계열프레임워크에서는테스트의성공 / 실패를판단하는데있어기본적으로 1:1 값비교가기반이된다. 예상값과실제수행값, 두값을놓고, 동등 (equal), 등가 (same value), 비교우위 (larger, smaller) 등의단순한연산을이용해테스트결과를판별한다. 경우에따라서는이런기본적인연산만으로는해당테스트자체의참 / 거짓을판별하기가다소불편할때가있는데, 위에서소개한 Matcher 를사용하면그런경우에도좀더편리하게값을비교할수있다. 이외에도많은 Matcher 가있으며, 계속추가되는추세다. 한가지아쉬운점은정적 (static) 으로되어있는 matcher 메소드의이름과소속클래스의이름을직관적으로생각해내기어려운경우가종종있다는점이다. 메소드자체와클래스이름에집중하기보다는해당라이브러리가지원해주는메소드의종류를살펴보는데좀더관심을기울이길권한다. 굳이외울필요까지는없고, 일반적인수준에서이러저러한기능을제공하는구나하는정도에서이해하면될듯싶다. 그래야하는이유중하나로, Hamcrest 에는묘한클래스가하나있는데, Machers 라는클래스다. 다음은 Matchers 라는클래스의소스일부다. 2 장 JUnit 과 Hamcrest 143
package org.hamcrest; public class Matchers { public static <T> org.hamcrest.matcher<t> allof( ) { return org.hamcrest.core.allof.<t>allof(first, second, third, fourth); public static <T> org.hamcrest.core.anyof<t> anyof( ) { return org.hamcrest.core.anyof.<t>anyof(matchers); public static <T> org.hamcrest.matcher<t> is(org.hamcrest.matcher<t> matcher) { return org.hamcrest.core.is.<t>is(matcher); Hamcrest 라이브러리는사용자의편의를위해일종의프론트매처클래스 (Front Matcher Class) 에해당하는 Matchers 라는클래스를만들어놓았다. 이클래스의메소드들은앞에서설명한 Matcher 메소드들과이름이중복되는데, 내부를살펴보면원래클래스들의메소드를호출하도록위임형태로작성되어있다. 따라서 Hamcrest 를사용할때는 org.hamcrest 패키지의 Matchers 클래스만 static import 처리해놓으면대부분의 Matcher 메소드를편하게사용할수있다. 그마저도불편하다면 JUnit 테스트템플릿을고쳐서자동으로 Matchers 클래스가 import 될수있게만들어놓으면된다. 144 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
Hamcrest 라이브러리를 JUnit 테스트케이스작성시템플릿으로지정해놓기 이클립스 Preferences 페이지에서 Java Editor Templates 를선택한다. Templates 종류중에서 Test라고되어있는항목을선택하고 Edit를선택한다. 다음과같이 importstatic 에 org. junit.matchers.junitmatchers.* 를추가한다. @${testtype:newtype(org.junit.test) public void ${testname() throws Exception { ${staticimport:importstatic('org.junit.assert.*', 'org.hamcrest.matchers.*')${cursor 주의! 이클립스갈릴레오버전에는 JUnit 4.5가탑재되어있고, Hamcrest 는 1.1 버전이들어있는데, 해당버전에서는 org.hamcrest.matchers 가존재하지않는다. Hamcrest 최신버전을내려받지않고현재포함된그대로사용하려면 org.hamcrest.matchers.* 대신에 org.hamcrest. CoreMatchers.* 를템플릿에넣어야한다. 현재 Hamcrest 라이브러리는원저자의의도와는별개로, 비교가핵심이되는테스트프레임워크에서많이쓰이고있으며, 향후에는일종의텍스트용정규식 (regular 2 장 JUnit 과 Hamcrest 145
expression) 처럼일반적으로쓰일가능성이높다. 앞으로는예제에서 Hamcrest 를이용한표현을때때로쓸예정이니그때그때형태를살펴보는걸로하자. 그런데 Hamcrest 라이브러리가항상편하기만한것은아니다. 때에따라서는 Hamcrest 를사용하는것이좀더불편하게느껴질때도있다. 아래는 Hamcrest 비교를사용한경우다. @Test public void testgetpage() throws Exception { manager.connect("www.doortts.com/csw /"); assertthat(manager.getpage(), is(expectedstring)); Hamcrest 의기본실패추적메시지 (Failure Trace) 형식에따라 Expected 와 got이위와같이표현되고있다. 하지만위실패메시지만으로는차이점이나원인등을제대로파악하기어렵다. 결국해당트레이스 (Failure Trace) 를복사한다음다른텍스트비교툴 (diff tool) 을써서비교할수밖에없다. 여간불편한게아니다. 그나마마우스오른쪽버튼을눌러서텍스트메시지를복사 (Copy Trace) 하는건가능하다. 146 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
다음은 assertthat ~ is 표현을제거하고 assertequals 만을사용한경우다. @Test public void testgetpage() throws Exception { manager.connect("www.doortts.com/csw/"); assertequals(manager.getpage(), expectedstring ); 테스트내용자체는동일하기때문에동일한실패가발생한다. assertequals 를사용했을때의 JUnit 의실패추적메시지 위그림은 assertequals 를사용해비교했을때의실패메시지다. 예상값 (expected) 과차이가나는부분을 [ ] 로감싸서보여주고있다. 거기에다이번엔이클립스에서자체비교툴을사용해서좀더가독성높은형태로볼수도있다. 이클립스에서실패추적메시지 (Failure Trace) 부분을마우스로더블클릭해보자. 다음과같은결과비교창이바로뜬다. 2 장 JUnit 과 Hamcrest 147
아! 인코딩문제였구나! IDE 기능으로인해상쇄돼버리는 hamcrest 실패구문설명의장점 좀아쉽긴하지만, 이건이클립스라는강력한통합개발환경으로인해 Hamcrest 실패구문설명 (failure description) 의장점이다소퇴색된부분이다. 향후에는이클립스가 Hamcrest 의메시지도비교하는기능을지원해주길기대한다. 중급이상도전! 사용자정의 Matcher 만들기만일 Hamcrest 라이브러리에서제공하는 Matcher 외의자신만의비교구문 Matcher 를만들고싶다면, TypeSafeMatcher 를상속받아서 matchessafely, describeto 를재정의하면된다. TypeSafeMatcher를상속받은 IsNotANumber 클래스 import org.hamcrest.description; import org.hamcrest.factory; import org.hamcrest.matcher; import org.hamcrest.typesafematcher; public class IsNotANumber extends TypeSafeMatcher<Double> { @Override public boolean matchessafely(double number) { 148 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
return number.isnan(); public void describeto(description description) { description.appendtext("not a number"); @Factory public static <T> Matcher<Double> notanumber() { return new IsNotANumber(); 사용자가직접정의해서만든새로운 Matcher인 notanumber의실제사용방식 public void testsquarerootofminusoneisnotanumber() { assertthat(math.sqrt(-1), is(notanumber())); 2.3 정리 이번장에서는단위테스트프레임워크의기본이되는 JUnit 과이와함께사용하는 Matcher 라이브러리인 Hamcrest 에대해살펴봤다. 만일 JUnit 3과 JUnit 4 중에서하나만알아야한다면당연히 JUnit 4를추천한다. 그리고 Hamcrest 라이브러리는쓸수있다면최대한쓸것을권장한다. 비교표현구문도간결해지고, 테스트케이스의의미도좀더명확하게만들어준다. 만일 2장의주제에대해추가적인학습을원한다면 junit.org 사이트의뉴스와기사들을살펴보길권한다. 또한 JUnit 라이브러리구조자체에관심이있다면, http://junit. sourceforge.net/doc/cookstour/cookstour.htm 에서찾아볼수있다. 어떤디자인패턴이쓰였고, 어떤원리로동작하는지자세히설명하고있다. 2 장 JUnit 과 Hamcrest 149
좋은코드? 나쁜코드? 좋은소스와그렇지않은소스를구분하는방법은무엇일까? 여러가지기준이있을수있겠지만, 가장쉬운권장방법은어떤코드가자연스럽게읽힐수있는지살펴보는것이다. 무슨말인가하면, 소스코드를볼때한줄한줄의의미를생각하며따라가는식으로 분석한다 는느낌이들면좋지않은소스, 소설책읽듯이문맥을갖고 쉽게읽힌다 면좋은소스로구분하는것이다. 우리가 소스코드 라는걸작성하는목적이자, 프로그래밍언어가계속발전하는중요한이유중하나는, 컴퓨터와좀더잘대화하기위해서가아니라다른사람들과좀더잘대화하기위해서라는사실을잊지말자. 150 테스트주도개발 : 고품질쾌속개발을위한 TDD 실천법과도구
전문가인터뷰 김창준애자일컨설팅대표 실제 TDD를적용하고전파하고계신전문가분들께인터뷰요청을드렸습니다. 책안의지식 이아닌 살아있는기술로써의 TDD 에대한의견을묻고듣고나누고싶었기때문입니다. 인터뷰내용은가감없이원문그대로실었습니다. 혹, 어색한문장이나통일되지않은단어가보이더라도너그러운마음으로양해부탁드립니다. 이인터뷰는김창준, 변신철, 황상철, 안영회, 박재성, 이일민님순으로진행됩니다. Q. 안녕하십니까? 대표님. 인터뷰에응해주셔서감사합니다. 대표님을아직잘모르는분들을위해간략한소개부탁드립니다. 그와함께현재하고계신일이나최근관심사에대해이야기해주실수있으신지요? A. 네. 저는애자일컨설팅의대표김창준입니다. 저희회사는애자일방법론을조직과개인에게컨설팅및코칭하는회사입니다. 최근에는애자일코치를키우는과정 (AC2, http:// ac2.kr) 에모든노력을기울이고있습니다. 우리나라에애자일이퍼지고성숙하려면수준높은애자일코치들이생기지않으면불가능하다는생각에일종의사명감을갖고노력하고있습니다. Q. 대표님께서 TDD 책을번역해서국내에본격적으로소개한지도적지않은시간이지났습니다. 하지만 TDD는여타기술이나기법에비해확산이느린것처럼보입니다. 이유가무엇이라고생각하시는지요? 혹시 TDD의가치가크지않아서확산이더딘건아닌지요? A. 몸에좋은약은쓴법입니다. TDD는개발자에게체질적변화를요구합니다. 그래서생각만큼확산이빠르지않습니다. 하지만점진적인변화는계속벌어지고있으며, 100도를넘으면산업전체의체질적변화가생길것이리라예측합니다. 실제로국내성공사례를보면 2차원에서 3차원으로의도약같은엄청난변화가있었습니다. 그리고 TDD 도입에어려움을겪는개인과조직은 TDD를좀더넓은의미로그리고원칙 ( 결정과피드백사이의갭을조정 ) 중심으로생각해볼필요가있습니다. Q. 혹기억에남는 TDD에대한경험담이나이야기를소개해주실수있으신지요? 좋은기억이아니어도무방합니다. A. 제가코칭한프로젝트에서 TDD의효과에대한실험을한적이있습니다. C 언어로개발했던금융거래프로젝트였습니다. TDD로개발한경우그렇지않은코드에비해모듈별개발시간이 15% 증가했습니다. 반면 TDD로개발한코드에서는결함이 60% 줄었습니다. 이프로젝트에서 TDD를하는가안하는가의선택은결국, 조금빨리개발하면서버그는 2.5배늘어나는방식 (TDD로개발하지않는방식 ) 을택하겠는가말겠는가의문제였습니다.
Q. 초급개발자가 TDD 를적용할때집중해야할부분이나유의해야할부분은무엇이라고생각하십니까? A. 모든단위테스트가통과하는시점에서다시모든단위테스트가통과하는시점까지걸리는시간을 GBC(Green Bar Cycle) 라고저는부릅니다. 이 GBC를짧게유지하는것이첫번째혈자리이고, 두번째혈자리는시스템의핵심을끝에서끝까지간단하게관통하는테스트가가급적일찍나오게하는것입니다. 이두가지혈자리를잘보존하면 TDD를성공적으로할수있습니다. Q. 강규영님과함께 TDD를통해 Line Reader를만들어가는동영상 * 은, 지금까지도많은이들이참고하는소중한자료중하나입니다. 혹시그동영상에대해아쉬운점이나, 생각이변해서지금이라면좀다르게접근할것같은부분은없으신지요? A. 촬영한것이 2003년입니다. 웹채팅서비스를 TDD로구현했는데, 그중에서일부분을보여드렸죠. 재미있게잘했다고생각합니다. 아쉬운점도몇가지가있지만하나만꼽는다면위에서언급한두번째혈자리를잘지키지못했다고생각합니다. 예를들자면아무것도없을때 (Empty) 를첫번째테스트케이스로잡았는데, 지금이라면그것보다뭔가있는케이스를먼저잡고전체를가볍게관통한다음, 부분들을키워나가는식을시도하겠습니다. Q. 만일개발을잘모르는관리자나경영층인사가 TDD가뭔가요? 라고묻는다면어떻게대답하십니까? A. 일단그런경우가많지가않았습니다만, 요즘은이렇게시작합니다. 개발기간은조금늘어나지만결함을획기적으로줄일수있는개발기법인데... Q. 굳이 TDD 관련내용이아니더라도, 좋은소프트웨어를만들고싶어하는 IT 개발자들에게해주고싶으신이야기나조언은없으신지요? A. 다음세가지질문을자신에게묻고솔직하게대답해볼것을권합니다. 내가어떻게해서이자리에있게되었는가? 내가이자리에있는것에대해어떻게느끼는가? 나는어떤일이일어나기를원하는가? Q. 귀한시간을내어인터뷰에응해주셔서감사합니다. A. TDD를소개하는좋은책을써주셔서감사합니다. 이책을통해우리나라에 TDD가더많이퍼지고, 개발자들이스트레스를덜받고자신의작업에대해뿌듯함을느끼며일하게되기를기원합니다. * 현재해당동영상은 XPer 커뮤니티의위키사이트 (http://xper.org/linereadertdd/) 에서찾아볼수 있습니다.