95 CHAPTER 4 테스트주도개발 제 대로동작할것을이미알고있는상황에서요구사항을충족시키기위한최소한의코드만 작성할수있다면정말멋지지않겠는가? 테스트주도개발 TDD: Test Driven Development 은개발자 가작성하는코드는올바르게동작해야하고, 요구사항을충족해야하며, 유지보수가쉬워야한다는소프트웨어개발에필요한모든것이포함된개념이다. 그이름에서알수있듯이 TDD를학습하다보면여러분의소프트웨어디자인이정확히의도한대로동작한다는것을보장할수있다는사실에놀라게될것이다. TDD의배경이되는개념은단하나이다. 즉, 실제코드를작성하기전에테스트코드를먼저작성하고테스트가실패하는지확인한후테스트에성공할수있도록코드를수정하고정리한다. 그리고이작업을반복하면된다. 이런작업은그림 4-1에서알수있듯이레드, 그린, 리팩토링주문이라고알려지고있다. TDD 방법론에따라코드를작성하려면우선실패하는테스트코드를작성하게된다. 테스트가실패하는이유는실제로배포할기능을구현하는코드가아니기때문이다. 대부분의단위테스트프레임워크는테스트가실패했음을알리기위해붉은색마크를테스트항목옆에표시하며, 이것이 레드 상태를의미하게된다. 테스트에실패하면여러분은테스트를통과할수있는최소한의코드를작성하여테스트를다시실행한다. 역시대부분의테스트프레임워크는테스트가성공했음을알리기위해녹색마크를표시한다. 이상태가바로 그린 상태이다. 이제리팩토링과정에서는코드를정리하고, 중복코드를제거하며, 유지보수가용이하도록코드를단순화하는과정을거치게되는데, 이미테스트는성공했기때문에리팩토링을실시해도좋은상태이며, 모듈의외부동작이아니라내부동작을수정할뿐이므로곧바로배포가가능한코드를구현할수
96 CHAPTER 4 테스트주도개발 있게된다. TDD 방법론을도입하면매우짧은주기로코딩작업을할수있으며, 이시점에여러분이작성하는프로그램에는단하나의요구사항만이존재하게된다. 간단한테스트코드를작성하고테스트를통과하기위한최소한의코드만작성한후리팩토링하고이과정을계속해서반복한다. 작은단위로작업을반복하는아이디어가바로핵심이며, 이로인해여러분은작고간결하며느슨하게결합된코드모듈을작성할수있게된다. 모든테스트를통과하게되면여러분의작업이완료되는것이며, 여러분의소프트웨어가의도된대로동작하는것을확인할수있는완벽한테스트코드들을갖추게된다. 리팩토링과정에서코드의동작이변경되지않았는지확인한다. 레드 실패하는테스트코드를작성한다. 리팩토링 그린 코드의중복을제거하여단순화한다. 테스트를통과하는코드를작성한다. 그림 4-1 지금쯤여러분은 TDD가매우현명한방법이라고생각하거나, 아니면존재하지도않는코드를위한테스트코드를작성하는것이어떻게소프트웨어의디자인에도움이되며소프트웨어의배포주기를향상시킬수있는지를알아내기위해머리를쥐어뜯고있을것이다. TDD가어떤이점을제공하는지를학습하기위해더많은내용들을읽어보는것도좋지만간단한 TDD 예제를실행해보는것이더좋을것이다. 이예제는 TDD가제공하는다른이점들에대해읽어보기전에전체적인프로세스를이해하는데도움이될것이다.
TDD 예제 : 틱택토게임 97 TDD 예제 : 틱택토게임 이제여러분은일명 3목두기라고알려진틱택토 Tic Tac Toe 게임을플레이할수있는프로그램을만들것이다. 이예제를통해여러분은 TDD가정확히무엇이며, 이것이애플리케이션디자인에어떤영향을미치는지학습하게될것이다. 프로그램을작성하기에앞서우선게임의규칙을이해하는것이중요하다. 특히이게임을해본적이없거나들어본적이없다면더욱중요하다. 틱택토는두사람이플레이할수있는간단한게임이다. 첫번째플레이어는 X로표시하며, 두번째플레이어는 O로표시한다. 틱택토게임의목적은 3행 3열로구성된보드에상대방보다먼저한줄을채우는것이다. 플레이어 X가항상먼저시작하며, 둘중한사람이한행이나한열혹은대각선방향으로자신의표시세개를나란히채우거나혹은보드에더이상표시를할수없을때까지교대로 X와 O를채워나가게된다. 9개의공간을모두채웠지만누구도자신의표시세개를나란히채우지못한경우에는게임은무승부가된다. TDD를연습하기전에요구사항의목록을작성해보거나혹은구현하고자하는애플리케이션의사용자시나리오를구성해보는것이좋다. 틱택토게임의요구사항 게임은 3행 3열로구성된보드에서진행된다. 게임은두명의플레이어가진행하며각각 X 표시와 O 표시를사용한다. 각각의플레이어는빈공간에자신의순서가되면표시를한다. 둘중한플레이어가한줄에세개의표시를나란히배치하면승리하게된다. 이한줄은한행이나한열혹은대각선이될수도있다. 모든공간이다채워졌으며승자가없다면게임은무승부가된다. TDD의관점에서요구사항목록만가지고는좋은단위테스트를구성할수없다. 단위테스트는일반적인요구사항보다는조금더구체적인내용을요구한다. 각각의테스트는의도된동작과알려진입력에대한특정한출력의예를표현할수있어야한다. 이요구사항을테스트를토대로다시정리해보자. 이기본적인테스트들은프로그램을구현해나가면서점차늘어나게되며작업을진행해나감에따라더많은테스트가필요해질것이분명하기때문에언제든지목록에추가하면된다.
98 CHAPTER 4 테스트주도개발 기본적인테스트먼저플레이를시작하는플레이어는 X이다. 플레이어는이미채워진공간에는표시를할수없다. 플레이어 X가빈공간에 X 표시를하면이공간은더이상사용할수없다. 플레이어 O는플레이어 X가표시를한후에표시할수있다. 플레이어는존재하지않는공간에는표시할수없다. 이미사용된공간에표시를하려고하면예외가발생한다. 플레이어 X가한행에세개의 X 표시를모두하면플레이어 X가승리한다. 플레이어 X가한열에세개의 X 표시를모두하면플레이어 X가승리한다. 플레이어 X가대각선으로세개의 X 표시를모두하면플레이어 X가승리한다. 모든공간이채워지고승자가없으면게임은무승부가된다. 우선첫번째테스트코드를작성하기에앞서우선프로젝트를준비해야한다. 이예에서는 NUnit 테스트프레임워크를사용하기로하자. 1. 우선 http://www.nunit.org를방문하여 NUnit 프레임워크의최신버전을다운로드하자. 이글을작성하는시점의가장최신버전은 NUnit 2.4.8이다 ( 옮긴이 _ 이글을번역하는시점의 가장최신버전은 2.5.5 이다. 그러나예제를구현하기에는아무런차이가없다 ). 2. 프레임워크를다운로드했으면 MSI 인스톨파일을실행한다. NUnit이설치되고나면 Program Files 폴더에새폴더가생성될것이다. 이제 NUnit 프레임워크의설치를완료했으므로테스트코드의작성을시작해보자. 1. Visual Studio를실행하고그림 4-2와같이 C:\Projects 디렉터리에 ProEnt.Chap4라는이름의빈솔루션을생성한다. 2. 다음의절차를따라솔루션에클래스라이브러리프로젝트를추가한다. 1. 파일메뉴에서 [ 추가 새프로젝트 ] 메뉴를선택한다. 2. 클래스라이브러리프로젝트템플릿을선택한다. 3. 프로젝트의이름을 ProEnt.Chap4.TicTacToe.Model로지정한다. 3. 동일한방식으로두번째클래스라이브러리프로젝트를 ProEnt.Chap4.TictacToe.ModelTests 라는이름으로추가한다.
TDD 예제 : 틱택토게임 99 그림 4-2 4. 모든프로젝트를추가했으면솔루션아이템을마우스오른쪽버튼으로클릭한후 [Windows 탐색기에서폴더열기 ] 메뉴를선택한후 lib라는이름의폴더를추가하고 NUnit.Frame work.dll 파일을 NUnit 프레임워크가설치된디렉터리에서복사한다 ( 이경로는기본적으로 %systemdrive%\%programfiles directory%\nunit 2.4.8\bin 폴더이다 ). 5. 이제솔루션에 Lib라는새로운솔루션폴더를추가한다. 그러려면솔루션아이템을마우스오른쪽버튼으로클릭한후 [ 추가 새솔루션폴더 ] 메뉴를선택한다. 6. 새로추가한 Lib 폴더를마우스오른쪽버튼으로클릭하고 [ 추가 기존항목 ] 메뉴를클릭한후애플리케이션의루트에생성했던 lib 폴더에있는 NUnit.Framework.dll 파일을선택한다. ProEnt.Chap4.TicTacToe.ModelTests 프로젝트를마우스오른쪽버튼으로클릭하고 [ 참조추가 ] 메뉴를선택하여 ProEnt.Chap4.TicTacToe.ModelTests 프로젝트에 NUnit. Framework.dll 파일을추가한다. 새참조추가대화상자에서 [ 찾아보기 ] 탭을선택하여 Lib 폴더를탐색한후 NUnit.Framework.dll 파일을선택하고 [ 참조추가 ] 버튼을클릭하면된다.
100 CHAPTER 4 테스트주도개발 7. 마지막으로 ProEnt.Chap4.TicTacToe.Model 프로젝트를마우스오른쪽버튼으로클릭하고 [ 참조추가 ] 메뉴를클릭하여 ProEnt.Chap4.TicTacToe.ModelTests 프로젝트에대한참조를추가한다. 참조추가대화상자에서 [ 프로젝트 ] 탭을선택하고 ProEnt.Chap4. TicTacToe.Model 프로젝트를선택한다. 지금까지의과정을마친후의솔루션탐색기의모습은그림 4-3과같다. 그림 4-3 ProEnt.Chap4.ticTacToe.ModelTests 프로젝트에 SimpleTest라는이름의클래스를생성하고다음의코드를작성한다. using System; using NUnit.Framework; namespace ProEnt.Chap4.TicTacToe.ModelTests [TestFixture()] public class SimpleTest
TDD 예제 : 틱택토게임 101 [Test()] public void My_First_NUnit_Test() int expectedresult = 3; Assert.AreEqual(expectedResult, 1 + 1); SimpleTest 클래스에 [TestFixture] 특성을지정하면이클래스가테스트코드를갖추고있음을 NUnit 프레임워크에알려주는셈이된다. 테스트메서드들은항상 public 메서드여야하며, 매개변수와리턴값이없어야하고, [Test] 특성이지정되어있어야한다. Assert 클래스가제공하는 static 메서드들은다양한방법으로현재의상태가올바른지를검증하기위해사용된다. AreEqual 메서드는테스트를통해기대하는값과실제값이동일한지를비교한다. 프로젝트를빌드하고시작메뉴에서 NUnit 애플리케이션을클릭하여실행한후 Program Files 폴더의 NUnit 폴더를열어보자. NUnit 애플리케이션에서 [File Open Project] 메뉴를선택한후 ProEnt.Chap4.TicTacToe.ModelTests 프로젝트의 Debug 폴더를탐색하여 ProEnt.Chap4.TicTacToe.ModelTests.dll 파일을선택한다. ProEnt.Chap4.TicTacToe.ModelTests. dll 파일이열리면그림 4-4와같은테스트트리를볼수있을것이다. 그림 4-4
102 CHAPTER 4 테스트주도개발 이제그림 4-5 와같이 Run 버튼을클릭하자. 그림 4-5 테스트결과 3이출력되기를원했지만실제결과는 2가출력되었기때문에테스트는당연히실패할것이다. VisualStudio로되돌아가코드를다음과같이수정하자. using System; using NUnit.Framework; namespace ProEnt.Chap4.TicTacToe.ModelTests [TestFixture()] public class SimpleTest [Test()] public void My_First_NUnit_Test() int expectedresult = 3; Assert.AreEqual(expectedResult, 2 + 1); 프로젝트를다시빌드하고그림 4-6 과같이테스트를다시실행하자.
TDD 예제 : 틱택토게임 103 그림 4-6 이제코드는테스트를통과하며녹색신호가나타나는것을볼수있다. 이장의후반부에서여러분은사용가능한다른테스트프레임워크와 NUnit과함께 Visual Studio에통합하여사용할수있는프로그램에대해학습하게될것이다. 이제 NUnit 프레임워크를사용하는방법에대해간략하게살펴보았으므로 TDD 방법론을토대로틱택토게임을구현해보도록하자. 앞서구성했던테스트목록중첫번째테스트는 먼저플레이하는플레이어는 X이다 이므로이것을테스트의제목으로사용할수있다. 테스트의이름을서술적으로표기하여이테스트가어떤내용을테스트하는지쉽게알아볼수있도록하는것이좋으며, 특히테스트항목이늘어나는경우에는이런방법이더욱유용하다. 테스트의이름을지정할때사용할수있는좋은방법은우선무엇을테스트할것인지를나열한후다음으로어떤동작을하는지를나열하는것이다. 바로 먼저플레이하는플레이어는 X이다 라는문장이테스트의제목으로적당하다. 테스트를작성할때프로그램을위한 API를간단하고유연하게작성할수있다. 여러분이구현이완료된소프트웨어를사용할사용자라고가정해보라. 이프로그램을어떻게사용할것인가? 여러분은소프트웨어를위한완벽한인터페이스를제공해야하는위치에있으므로테스트를작성할때는가급적간단하고논리적으로구현해야한다는것을항상염두에두어야하며, 그렇게함으로써소프트웨어에서유용하게활용될수있는코드를작성하게된다. 그러면틱택토게임의모든규칙을확인하기위한테스트코드를작성할클래스를생성해보자. ProEnt.Chap4. TicTacToe.ModelTests 프로젝트에 TicTacToeGameTests라는이름의클래스를생성한다.
104 CHAPTER 4 테스트주도개발 앞서설명했던대로첫번째테스트메서드의이름은 Player_X_Is_The_First_To_Place_ A_Marker 라고지정하고다음의코드를작성한다. using System; using NUnit.Framework; using ProEnt.Chap4.TicTacToe.Model; namespace ProEnt.Chap4.TicTacToe.ModelTests [TestFixture] public class TicTacToeGameTests [Test] public void Player_X_Is_The_First_To_Place_A_Marker() TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.AreEqual(player.x, agameoftictactoe.whoseturn()); 여러분은코드를컴파일하지는않았지만이미소프트웨어의디자인에있어몇가지큰결정을내린셈이다. 여러분은플레이어의 ID를열거자로표현하도록결정했으며, 메서드가현재순서가된플레이어의 ID를리턴하도록정의했다. 또한 TicTacToeGame 객체의새인스턴스가생성될때게임이시작되도록결정했다. 이런모든결정들은소프트웨어가이미구현이됐다면이소프트웨어를사용할사람의입장에서결정된것이다. 이렇게함으로써 API 자체를쉽게사용할수있도록구성할수있다. 물론방금작성한테스트는컴파일되지않을것이므로약간의코드를더추가하여테스트를컴파일할수있도록한후실패하도록만들어야한다. 지금은실제동작하는코드를작성하기에앞서테스트코드를작성하려는것이기때문에이테스트가실패하도록구현하는것이현재로는가장중요하다. 테스트를컴파일하려면 TicTacToeGame 클래스의뼈대코드와플레이어 X를정의하기위한열거자를구현해야한다. 이제 Player라는이름의열거자와 TicTacToeGame 클래스를다음과같이구현한다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame
TDD 예제 : 틱택토게임 105 public TicTacToeGame() public player WhoseTurn() return player.o; namespace ProEnt.Chap4.TicTacToe.Model public enum player x = 1, o = 2 기억할것은여러분의코드가테스트를통과하기전에우선실패하는테스트를만들어야한다는점이기때문에 WhoseTurn() 메서드가 player.x를리턴하도록수정하고싶더라도일단은참아야한다. 프로젝트를컴파일하고테스트를수행하면그림 4-7과같이테스트가실패하게된다. 바로이것이우리가원하는결과이다. 이제테스트가성공할수있도록코드를수정해보자. 그림 4-7
106 CHAPTER 4 테스트주도개발 이제 WhoseTurn() 메서드가 player.x를리턴하도록수정하자. 테스트프로젝트를다시빌드하고 NUnit 테스트프로그램에서다시테스트하면그림 4-8과같이테스트가성공하게된다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame public TicTacToeGame() public player WhoseTurn() return player.x; 그림 4-8 이시점에서여러분은게임이진행되는동안 WhoseTurn() 메서드가항상플레이어 X를리턴할수없기때문에방금작성한테스트코드가올바른것이아니라고생각할수있다. 물론여러분이맞다. 그러나어떤일이일어날지혹은소프트웨어가어떤일을해야하는지에대해고민할필요는없다. 여러분이지금해야하는일은최대한간단한방법으로코드가테스트를통과할
TDD 예제 : 틱택토게임 107 수있도록구현하는것이므로다음단계로진행해보자. 새로운테스트가필요하다고생각된다면테스트목록에추가하고작업을계속하자. 이때테스트에필요치않은기능을구현하려고시도하지말자. 여러분은조금씩앞으로나아가야한다. 이예제를구현해나가면서이간단한예제가무엇을보여주고싶어하는지알수있게될것이므로조금만참아보도록하자. 지금까지는매우간단한코드를작성했으므로리팩토링할것도없으므로곧바로다음테스트를진행한다. 처음에작성했던테스트목록을살펴보면다음테스트는 플레이어는이미채워진공간에는표시를할수없다 이다. 이에대해생각해보면 첫번째플레이어는어디에든표시할수있다 라는점에대해테스트를수행하는것이더좋은생각일것이다. 따라서 첫번째플레이어는어디에든표시할수있다 라는테스트를다음과같이추가한다. [Test] public void The_First_Player_Can_Place_Marker_Anywhere() TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(0, 0)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(0, 1)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(0, 2)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(1, 0)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(1, 1)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(1, 2)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(2, 0)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(2, 1)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(2, 2)); 흠, 지금작성한코드와디자인에대해별다른이상한낌새는없는가? 게임에사용된보드가 0부터시작하는인덱스로구성되었다는것은좋은생각일까? 또한첫번째정수는행을의미할까아니면열을의미할까? 이코드를사용한다면직접사용해보고에러가발생해야제대로된사용법을배울수있다. 그렇다면여러분의의도를충분히표현할수있게코드를작성해야하지않을까? 이제이코드를쉽게이해할수있도록다시작성해보자. [Test] public void The_First_Player_Can_Place_Marker_Anywhere() TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.One,
108 CHAPTER 4 테스트주도개발 Column.One)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.One, Column.Two)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.One, Column.Three)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Two, Column.One)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Two, Column.Two)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Two, Column.Three)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Three, Column.One)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Three, Column.Two)); Assert.IsTrue(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Three, Column.Three)); 이코드는훨씬가독성이높다. 그렇지않은가? 리팩토링은아주간단했지만어떤매개변수가무엇인지정확히의미하며, 다른개발자가보드의인덱스가 0부터시작하는지아닌지추측할필요가없도록해준다. 이테스트는마찬가지로컴파일되지않을것이므로먼저해야할일은컴파일을방해하는요소들을정리하는것이다. 우선 Column과 Row라는두가지열거자클래스를생성한후다음과같이코드를작성한다. namespace ProEnt.Chap4.TicTacToe.Model public enum Column One = 0, Two = 1, Three = 2 namespace ProEnt.Chap4.TicTacToe.Model public enum Row One = 0, Two = 1,
TDD 예제 : 틱택토게임 109 Three = 2 두개의열거자를새로추가했으면 TicTacToeGame 클래스의메서드를다음과같이수정하여테스트가컴파일되도록하자. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame public TicTacToeGame() public player WhoseTurn() return player.x; public bool CanPlaceMarkerAt(Row row, Column column) return false; 다시말하지만, 우선은테스트가컴파일되어도성공하지못하도록해야하므로, CanPlace MarkerAt 메서드가 false를리턴하도록하드코딩된것이다소이해하기는어렵더라도현재로서는우리가원하는결과이다. 모델프로젝트를다시빌드하면테스트는컴파일될것이며, 테스트를실행하면실패하게된다. 이것이바로우리가원했던결과이다. 이제최소한의코드변경으로테스트가성공하도록수정해보자. 테스트가성공할수있는가장간단한방법은하드코딩된 false 리턴값을 true로변경하는것이다. 우선다음과같이코드를변경해보자. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame public TicTacToeGame()
110 CHAPTER 4 테스트주도개발 public player WhoseTurn() return player.x; public bool CanPlaceMarkerAt(Row row, Column column) return true; 테스트를다시실행하면이번에는테스트에성공할것이다. 지금여러분은 TDD의장점에대해알게되기는커녕시간만낭비했다고생각할지도모르겠다. 그러나이예제를통해여러분이학습한것은소프트웨어가올바른동작을수행하는지확인하기위한테스트코드를작성해야한다는것이다. 지금까지처음두개의테스트를통과하기위한코드를작성했다. 앞으로도여러분이작성하는코드가최초의테스트목록을통과할수있는지를확인하기위한테스트들을추가하게될것이다. 이런과정을테스트측정이라고한다. 측정 triangulation 이란, 여러개의서로다른테스트를작성하여다양한관점에서테스트가성공하는지를확인하는과정을말한다. 다시말하면, WhoseTurn 메서드와 CanPlaceMarkerAt 메서드가올바르게동작하는지를확인하기위한또다른테스트코드를작성해야한다는뜻이다. 이제다음테스트는 플레이어 X 가빈공간에 X 표시를하면이공간은더이상사용할수없다 라는항목이다. [Test] public void After_A_Player_Places_A_Marker_The_Square_Is_Unavailable() Row rowtoplace = Row.One; Column columntoplace = Column.One; TicTacToeGame agameoftictactoe = new TicTacToeGame(); agameoftictactoe.placemarkerat(rowtoplace, columntoplace); Assert.IsFalse(aGameOfTicTacToe.CanPlaceMarkerAt(rowToPlace, columntoplace));
TDD 예제 : 틱택토게임 111 이전테스트와마찬가지로아직 PlaceMarkerAt 메서드를작성하지않았기때문에테스트를컴파일하려면이메서드부터구현해야한다. 테스트프로젝트가참조하고있는 TicTacToeGame 클래스에 PlaceMarkerAt 메서드를추가하고다음의코드를작성한후 Model 프로젝트를다시빌드한다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame public TicTacToeGame() public player WhoseTurn() return player.x; public bool CanPlaceMarkerAt(Row row, Column column) return false; public void PlaceMarkerAt(Row row, Column column) 아마도이코드를컴파일하고테스트를실행하면분명실패할것이라고예상할것이다. 그러면테스트에성공하도록코드를작성해보자. 테스트코드는코드의동작을확인하기위해 TicTacToeGame 클래스의두개의메서드를사용하고있으므로이두메서드를모두수정해야한다. 그러나완전히새로게임을시작했을때 CanPlaceMarkerAt 메서드의동작을확인하기위한테스트메서드가이미존재하므로이메서드는이제 false만을리턴하도록하드코딩되어서는안되며, 이제는게임보드에존재하는각공간의상태를관리해야한다. 그러려면 3행 3열의게임보드를표현할다차원배열을생성해야한다. 지금해야할일은테스트코드를측정하여코드의동작을확인하기위한하나의테스트에만의존하지않도록하는것이다. 보다자세한내용은다음에다시살펴보도록하자.
112 CHAPTER 4 테스트주도개발 어쨌든지금은 TicTacToeGame 클래스를다음과같이수정하자. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame private int[,] Board = new int[3, 3]; public TicTacToeGame() public player WhoseTurn() return player.x; public bool CanPlaceMarkerAt(Row row, Column column) return this.board[(int)row, (int)column] == 0; public void PlaceMarkerAt(Row row, Column column) this.board[(int)row, (int)column] = (int)whoseturn(); 단세줄의코드를추가했을뿐이지만이전의코드와비교해보면큰폭으로달라진것을알수있다. 첫번째는 3행 3열의게임보드에표시된값을관리하기위해다차원배열을생성한것이다. 그런후 CanPlaceMarkerAt 메서드를수정하여전달된값을정수로변환하여주어진위치의값이 0인지를비교하는데, 이경우해당위치에표시가있다는뜻일까? 마지막으로 PlaceMarkerAt 메서드에지정된위치에 WhoseTurn 메서드가리턴한값을대입한다. 이코드를컴파일하고지금까지작성한세개의테스트를모두수행해보면코드가수정되었기때문에이전테스트도순조롭게통과하는것을볼수있을것이다. 이때변경한코드가전체테스트에영향을미치지않는지를확인하기위해모든테스트를실행해보는것이중요하다. 모든테스트를실행하고모두테스트에통과한다면다행이다. 이렇듯 TDD를이용하면변경된코드가다른부분에영향을미치는지여부를즉시알수있게된다.
TDD 예제 : 틱택토게임 113 코드를살펴보면알겠지만, CanPlaceMarkerAt 메서드가 false를리턴할경우플레이어가원하는위치에표시할수없도록하는방법은없다. 이런현상을방지하기위해코드를추가로작성할수는있겠지만우선은테스트를먼저작성한후에이동작을수행하는것이더좋다. 다시한번말하지만여러분은테스트를통과하기위한코드만을작성해야한다. 그렇다면이를위한테스트코드를작성해보자. 플레이어가원하는위치에표시를할수있는지를검사하는메서드가이미존재하기때문에사용자가이를무시하려하면프로그램이예외를발생시키는것이논리적이다. 따라서 TicTacToeGameTests 클래스에새로운테스트를다음과같이추가하여문제가되는상황에서코드가적절히동작하는지확인해보자. [Test] [ExpectedException(typeof(System.ApplicationException), "Square Row:One, Column:One already occupied by x" )] public void Exception_Will_Be_Thrown_If_Player_Tries_To_Place_Marker_In_A_Taken_Square() Row rowtoplace = Row.One; Column columntoplace = Column.One; TicTacToeGame agameoftictactoe = new TicTacToeGame(); agameoftictactoe.placemarkerat(rowtoplace, columntoplace); agameoftictactoe.placemarkerat(rowtoplace, columntoplace); 이테스트는지금까지작성했던것과는조금다르다. 이테스트메서드는두번째특성이지정되어있으며, 입력값에대한유효성검사를수행하는코드는없다. 이메서드에지정된 Expected Exception 특성은 NUnit 프레임워크가지정된메시지를가진예외가발생하는경우에테스트를통과한것으로처리하도록하기위한것이다. 테스트를실행해보면실패하게된다. 이제플레이어가표시하려고하는공간이실제로비어있는지를검사하여비어있지않다면적당한예외를발생시키는코드를추가하자. 수정된 Place MarkerAt 메서드의코드는다음과같다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame private int[,] Board = new int[3, 3]; public TicTacToeGame()
114 CHAPTER 4 테스트주도개발 public player WhoseTurn() return player.x; public bool CanPlaceMarkerAt(Row row, Column column) return this.board[(int)row, (int)column] == 0; public void PlaceMarkerAt(Row row, Column column) if (CanPlaceMarkerAt(row, column)) this.board[(int)row, (int)column] = (int)whoseturn(); else throw new ApplicationException( string.format("square Row:0, Column:1 already occupied by 2", row.tostring(), column.tostring(), (player)enum.toobject(typeof(player), this.board[(int)row, (int)column]))); TDD의규칙을곧이곧대로해석한다면여러분은테스트를통과할수있는가장간단한코드를작성하려할것이며, 따라서 CanPlaceMarkerAt 메서드를다음과같이작성할것이다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public void PlaceMarkerAt(Row row, Column column) if (CanPlaceMarkerAt(row, column))
TDD 예제 : 틱택토게임 115 this.board[(int)row, (int)column] = (int)whoseturn(); else throw new ApplicationException( "Square Row:One, Column:One already occupied by x") 그러나알다시피이예외의에러메시지에는문제가발생한행과열번호를동적으로삽입할필요가있으므로, 순수한의미의 TDD와비교해약간의절충안을취하여다시실용적인관점에서접근할수있다. 하지만그럼에도불구하고여러분은계속해서조금씩단계를밟아나가야하며, 결코걷기전에뛰려고하거나혹은하나의테스트에서너무많은작업을하려고해서는안된다. 여러분은테스트에성공할수있는코드를작성하려는것이지전체프로그램을작성하려는것이아님을다시한번기억하기바란다. 다시말하면, 우선규칙을이해해야무엇을할지알수있게되며, 그런후에야작업속도를올릴수있게된다. 지금필자가바라는것은여러분이지금처럼 TDD의규칙을약간어김으로써얻을수있는이점이무엇인지를파악하는것이다. 여러분은테스트를통과하기위한최소한의코드를작성한것은아니지만그렇다고해서디자인전체를복잡하게하지도않았다. 여러분은행과열의조합이언제든지변경될수있다는것을알기때문에나중에이전테스트코드가동작하지않게될경우, 필요한또다른테스트코드를작성하는대신이를처리할수있도록테스트코드를작성하겠다는주관적인판단을내린것뿐이다. 이런과정은순수하게학술적인 TDD에대한접근법에는다소어긋난것이지만이번에사용한방법은상식적인면에서크게벗어나지않는다. TDD에대해더많이경험하다보면시스템이테스트를필요로하지않는부분이어떤부분인지혹은어디쯤에서규칙을약간어겨도무방한지를명확하게판단할수있게될것이다. 이섹션의나머지부분들은지금경험한내용을토대로한다. 이제여러분이방금무슨일을했는지이해했으므로틱택토게임의나머지기능들을계속해서구현해보자. 테스트목록으로돌아가보면다음테스트항목은 플레이어 O는플레이어 X가표시를한후에표시할수있다 이다. 이테스트는 WhoseTurn 메서드가올바르게동작하지않는다는점을밝혀내기위한것이다. 이런방법은이미테스트가완료되었다고생각한부분을다시한번확인하는좋은방법이다. 그러면 TicTacToeGameTests 클래스에다음의테스트메서드를추가해보자.
116 CHAPTER 4 테스트주도개발 [Test] public void Player_O_Will_Be_Next_To_Take_A_Turn_After_Player_X_Has_Placed_A_ Marker() Row rowtoplace = Row.One; Column columntoplace = Column.One; TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.AreEqual(player.x, agameoftictactoe.whoseturn()); agameoftictactoe.placemarkerat(rowtoplace, columntoplace); Assert.AreEqual(player.o, agameoftictactoe.whoseturn()); 이제테스트를실행해보면테스트가실패하는것을볼수있는데, 그이유는 WhoseTurn() 메서드가앞서작성했던테스트를성공하기위해항상 player.x를리턴하도록하드코딩되어있기때문이다. 이제이코드를리팩토링하여현재보드에표시를할수있는실제플레이어를리턴하도록수정해보자. 그러려면현재플레이어를보관할변수를추가하고, 생성자에서이변수에플레이어 X를지정한후플레이어 X가보드에표시를하면다른플레이어로변경하면된다. 다음의코드는현재플레이어를표시하기위해수정된코드를표시하고있다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame private int[,] Board = new int[3, 3]; private player currentplayer; public TicTacToeGame() currentplayer = player.x; public player WhoseTurn() return currentplayer; public bool CanPlaceMarkerAt(Row row, Column column) return this.board[(int)row, (int)column] == 0;
TDD 예제 : 틱택토게임 117 public void PlaceMarkerAt(Row row, Column column) if (CanPlaceMarkerAt(row, column)) this.board[(int) row, (int) column] = (int) WhoseTurn(); if (this.currentplayer == player.x) this.currentplayer = player.o; else this.currentplayer = player.x; else throw new ApplicationException( string.format( "Square Row:0, Column:1 already occupied by 2", row.tostring(), column.tostring(), (player)enum.toobject(typeof(player), this.board[(int)row, (int)column]))); 이제모든테스트를실행해보면테스트가모두통과한다는것을알수있다. 그런데 Place MarkerAt 메서드의코드가점점늘어나고있다. 이때여러분은현재플레이어를변경하는로직을별도의메서드로리팩토링하여코드를더욱읽기쉽게만들수있다. 현재플레이어를검사하여플레이어를변경하는부분의로직을선택하고오른쪽버튼을클릭한후 [ 리팩토링 메서드추출 ] 메뉴를선택한다. 메서드추출대화상자에서는새로추가할메서드의이름을 Change CurrentPlayer라고입력한다. 변경된코드는다음과같다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public void PlaceMarkerAt(Row row, Column column)
118 CHAPTER 4 테스트주도개발 if (CanPlaceMarkerAt(row, column)) this.board[(int) row, (int) column] = (int) WhoseTurn(); ChangeCurrentPlayer(); else throw new ApplicationException( string.format( "Square Row:0, Column:1 already occupied by 2", row.tostring(), column.tostring(), (player)enum.toobject(typeof(player), this.board[(int)row, (int)column]))); private void ChangeCurrentPlayer() if (this.currentplayer == player.x) this.currentplayer = player.o; else this.currentplayer = player.x; 이제다시한번모든테스트를실행하여조금전실행했던리팩토링으로인해영향을받은부분이없는지확인하자. 다음으로구현할테스트항목은 플레이어는존재하지않는공간에는표시할수없다 라는항목이다. 이동작을검증할테스트를다음과같이작성하면이테스트는컴파일에실패하게된다. [Test] public void A_Player_Cannot_Place_A_Marker_In_A_Zone_That_Does_Not_Exist() TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.IsFalse(aGameOfTicTacToe.CanPlaceMarkerAt(33, 11));
TDD 예제 : 틱택토게임 119 이코드가컴파일이되지않는이유는여러분이 row와 column 변수값을위해열거자를사용하기로했기때문이며, 이부분은처음에테스트목록을구성할때고려되지않았던것이다. 이와같은디자인결정으로인해사용자들은오직유효한행과열위치를표시하는값만을사용할수있음이검증되었으므로이테스트코드는주석으로처리하고다음테스트를진행하도록하자. //[Test] //public void A_Player_Cannot_Place_A_Marker_In_A_Zone_That_Does_Not_Exist() // // TicTacToeGame agameoftictactoe = new TicTacToeGame(); // Assert.IsFalse(aGameOfTicTacToe.CanPlaceMarkerAt(33, 11)); // 다음테스트는게임의승자를가려내기위한것으로 플레이어 X가한행에세개의 X 표시를모두하면플레이어 X가승리한다 라는항목이다. [Test] public void If_Player_X_Gets_Three_Xs_In_A_Row_Then_The_Game_Is_Won_By_Player_X() Row PlayerX_RowMove123 = Row.One; Column PlayerX_ColumnMove1 = Column.One; Column PlayerX_ColumnMove2 = Column.Two; Column PlayerX_ColumnMove3 = Column.Three; Row PlayerO_RowMove12 = Row.Two; Column PlayerO_ColumnMove1 = Column.One; Column PlayerO_ColumnMove2 = Column.Two; TicTacToeGame agameoftictactoe = new TicTacToeGame(); // Player X Move 1 // X // // agameoftictactoe.placemarkerat(playerx_rowmove123, PlayerX_ColumnMove1); // Player O Move 1 // X // O // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove1); // Player X Move 2 // X X
120 CHAPTER 4 테스트주도개발 // O // agameoftictactoe.placemarkerat(playerx_rowmove123, PlayerX_ColumnMove2); // Player O Move 2 // X X // O O // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove2); // Player X Move 3 // X X X // O O // agameoftictactoe.placemarkerat(playerx_rowmove123, PlayerX_ColumnMove3); Assert.AreEqual(GameStatus.PlayerXWins, agameoftictactoe.status()); 이코드가컴파일이되도록하려면현재상태를표시할열거자클래스를추가해야할뿐아니라게임의현재상태를리턴하는메서드도추가해야한다. 틱택토게임은다음의다섯가지상태중하나만을가질수있다. 1. 플레이어 X 가승리했다. 2. 플레이어 O 가승리했다. 3. 게임이비겼다. 4. 플레이어 X 가표시할차례이다. 5. 플레이어 O 가표시할차례이다. 그러면 Model 프로젝트에 GameStatus라는이름의 C# 소스파일을추가하고다음과같이열거자를정의해보자. namespace ProEnt.Chap4.TicTacToe.Model public enum GameStatus PlayerXWins = 1, PlayerOWins = 2, GameDrawn = 3,
TDD 예제 : 틱택토게임 121 AwaitingPlayerXToPlaceMarker = 4, AwaitingPlayerOToPlaceMarker = 5 이제게임의현재상태를리턴하는메서드를다음과같이추가하면테스트가컴파일될것이다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status() return GameStatus.GameDrawn; 자, 이제여러분은컴파일은되지만실패하는테스트코드를구현했다. 다시말하지만, TDD의규칙을정확하게따르고자한다면여러분은테스트에통과할수있는최소한의코드만을작성해야하며, 다시말해 Status 메서드를다음과같이수정해야한다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status() return GameStatus.PlayerXWin; 그러나여러분은 TDD의철학을따르기위해서는규칙을지키는것이좋지만그것이법은아니기에반드시그렇게해야하는것은아니라는점을알고있을것이다. 따라서시간을보다효율적으로활용하려면게임에서누군가승리했는지를판단하기위해각각의행을검사하는코드를작성하는것이좋다.
122 CHAPTER 4 테스트주도개발 namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status() GameStatus GameStatus = Model.GameStatus.GameDrawn; for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player.x && this.board[row, (int)column.two] == (int)player.x && this.board[row, (int)column.three] == (int)player.x)) GameStatus = GameStatus.PlayerXWins; if ((this.board[row, (int)column.one] == (int)player.o && this.board[row, (int)column.two] == (int)player.o && this.board[row, (int)column.three] == (int)player.o)) GameStatus = GameStatus.PlayerOWins; return GameStatus; 이렇게변경한코드는테스트에는통과하지만그다지보기에는좋지않으며, 향후에여러분이검사해야할다른상태를리턴해야할필요가있으므로리팩토링이필요하다. 메서드추출리팩토링신공으로 Status 메서드를다음과같이정리해보자. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status()
TDD 예제 : 틱택토게임 123 GameStatus GameStatus = Model.GameStatus.GameDrawn; if (isawinner(player.o)) GameStatus = Model.GameStatus.PlayerOWins; else if (isawinner(player.x)) GameStatus = Model.GameStatus.PlayerXWins; return GameStatus; private bool isawinner(player Player) bool winner = false; for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player && this.board[row, (int)column.two] == (int)player && this.board[row, (int)column.three] == (int)player)) winner = true; return winner; 이제모든테스트를실행해보면모두테스트를통과할것이다. 마지막테스트코드를작성할때불현듯떠오른생각은게임의상태는누군가승리하거나혹은게임이비기지않은이상플레이어 X나플레이어 O가표시해야할순서인상태가있을수있다는것이다. 현재게임의상태는승자가없다면게임이비긴것으로표시되고있다. 이동작을확인하기위해서는코드를구현하기에앞서테스트코드를구현해야한다. 다음과같이새로운테스트메서드를추가해보자.
124 CHAPTER 4 테스트주도개발 [Test] public void The_Game_Status_Should_Be_Awaiting_Either_Player_X_Or_O_If_The_Game_Is_Not_Won_ Or_Drawn() Row PlayerXrowToPlace = Row.One; Column PlayerXcolumnToPlace = Column.One; TicTacToeGame agameoftictactoe = new TicTacToeGame(); Assert.AreEqual(GameStatus.AwaitingPlayerXToPlaceMarker, agameoftictactoe.status()); agameoftictactoe.placemarkerat(playerxrowtoplace, PlayerXcolumnToPlace); Assert.AreEqual(GameStatus.AwaitingPlayerOToPlaceMarker, agameoftictactoe.status()); 이테스트가성공하려면두플레이어중누군가게임에승리했는지를검사해야하며, 그렇지않다면누가표시할차례인지를알아내야한다. 따라서 Status 메서드는다음과같이변경되어야한다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status() GameStatus GameStatus = Model.GameStatus.GameDrawn; if (IsAWinner(player.o)) GameStatus = Model.GameStatus.PlayerOWins; else if (IsAWinner(player.x)) GameStatus = Model.GameStatus.PlayerXWins; else if (WhoseTurn() == player.x) GameStatus = Model.GameStatus.AwaitingPlayerXToPlaceMarker; else GameStatus = Model.GameStatus.AwaitingPlayerOToPlaceMarker; return GameStatus;
TDD 예제 : 틱택토게임 125... Model 프로젝트와 Test 프로젝트를다시빌드하고테스트를실행해보면승자가없거나게임이비기지않은상태에서도게임의상태가올바르게리턴되는것을확인할수있다. 다음테스트는 플레이어 X가한열에세개의 X 표시를모두하면플레이어 X가승리한다 라는항목이다. 이테스트를위한코드는앞서작성한코드와유사하지만이번에는각행별로검사를수행해야한다. 테스트코드는다음과같다. [Test] public void If_Player_X_Gets_Three_Xs_In_A_Column_Then_The_Game_Is_Won_By_Player_X() Row PlayerX_RowMove1 = Row.One; Row PlayerX_RowMove2 = Row.Two; Row PlayerX_RowMove3 = Row.Three; Column PlayerX_ColumnMove123 = Column.One; Row PlayerO_RowMove12 = Row.One; Column PlayerO_ColumnMove1 = Column.Two; Column PlayerO_ColumnMove2 = Column.Three; TicTacToeGame agameoftictactoe = new TicTacToeGame(); // Player X Move 1 // X // // agameoftictactoe.placemarkerat(playerx_rowmove1, PlayerX_ColumnMove123); // Player O Move 1 // X O // // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove1); // Player X Move 2 // X O // X
126 CHAPTER 4 테스트주도개발 // agameoftictactoe.placemarkerat(playerx_rowmove2, PlayerX_ColumnMove123); // Player O Move 2 // X O O // X // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove2); // Player X Move 3 // X O O // X // X agameoftictactoe.placemarkerat(playerx_rowmove3, PlayerX_ColumnMove123); Assert.AreEqual(GameStatus.PlayerXWins, agameoftictactoe.status()); 이테스트를실행해보면테스트에실패하게된다. 이는플레이어가승리했는지를검사하기위해수평으로각행을점검했기때문이다. 올바른테스트를위해서는세개의 X가각행에존재하는지확인하는코드를작성해야한다. TicTacToeGame 클래스의 IsAWinner 메서드를다음과같이수정하여 X 혹은 O 플레이어가승리했는지를각행별로검사하도록구현해보자. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... private bool IsAWinner(player Player) bool winner = false; for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player && this.board[row, (int)column.two] == (int)player && this.board[row, (int)column.three] == (int)player)) winner = true;
TDD 예제 : 틱택토게임 127 for (int Column = 0; Column <= 2; Column++) if ((this.board[(int)row.one, Column] == (int)player && this.board[(int)row.two, Column] == (int)player && this.board[(int)row.three, Column] == (int)player)) winner = true; return winner; 이제 Model 프로젝트와 Tests 프로젝트를다시빌드하고전체테스트를실행해보면모든테스트가통과하게될것이다. 이번테스트에서는 IsAWinner 메서드의분량이꽤커졌다. 따라서이메서드를리팩토링하는것이좋다. 다음테스트는플레이어 X가대각선으로승리의조건을만들었는지확인해야하므로일단테스트코드를작성하는것을중단하고 IsAWinner 메서드가모든조건에대응할수있도록수정하는것이좋겠다. [Test] public void If_Player_X_Gets_Three_Xs_In_A_Diagonal_Line_The_Game_Is_Won_By_Player_X() Row PlayerX_RowMove1 = Row.One; Row PlayerX_RowMove2 = Row.Two; Row PlayerX_RowMove3 = Row.Three; Column PlayerX_ColumnMove1 = Column.One; Column PlayerX_ColumnMove2 = Column.Two; Column PlayerX_ColumnMove3 = Column.Three; Row PlayerO_RowMove12 = Row.One; Column PlayerO_ColumnMove1 = Column.Two; Column PlayerO_ColumnMove2 = Column.Three; TicTacToeGame agameoftictactoe = new TicTacToeGame(); // Player X Move 1 // X // // agameoftictactoe.placemarkerat(playerx_rowmove1, PlayerX_ColumnMove1);
128 CHAPTER 4 테스트주도개발 // Player O Move 1 // X O // // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove1); // Player X Move 2 // X O // X // agameoftictactoe.placemarkerat(playerx_rowmove2, PlayerX_ColumnMove2); // Player O Move 2 // X O O // X // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove2); // Player X Move 3 // X O O // X // X agameoftictactoe.placemarkerat(playerx_rowmove3, PlayerX_ColumnMove3); Assert.AreEqual(GameStatus.PlayerXWins, agameoftictactoe.status()); 이테스트코드역시여전히실패할것이다. 이번에는대각선으로도승리한플레이어가있는지를검사하는코드를추가해야한다. 이코드는 IsAWinner 메서드에추가할것이다. 지면을절약하기위해테스트코드에서는우측상단으로부터좌측하단방향으로의대각선을검사하는코드는생략할것이며, 혹시다운로드한예제코드에도포함되어있지않다면여러분은해당코드를구현해야한다. 플레이어 O가승리했는지검사하는로직도이와동일하다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... private bool IsAWinner(player Player) bool winner = false;
TDD 예제 : 틱택토게임 129 for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player && this.board[row, (int)column.two] == (int)player && this.board[row, (int)column.three] == (int)player)) winner = true; for (int Column = 0; Column <= 2; Column++) if ((this.board[(int)row.one, Column] == (int)player && this.board[(int)row.two, Column] == (int)player && this.board[(int)row.three, Column] == (int)player)) winner = true; if (( this.board[(int)row.one, (int)model.column.one] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.three] == (int)player)) winner = true; if (( this.board[(int)row.one, (int)model.column.three] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.one] == (int)player)) winner = true; return winner; 이제승리를위한모든조건을만족하는코드를갖추게되었으므로 IsAWinner 메서드를리팩
130 CHAPTER 4 테스트주도개발 토링해보자. 메서드추출기법을이용하여승리한플레이어를검사하는로직을세개의메서드로분리할수있다. 이렇게함으로써코드의가독성을향상시켜더욱이해하기쉬운코드를작성할수있다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... private bool IsAWinner(player Player) bool winner = false; if (IsThreeInARowWinner(Player) IsThreeInAColumnWinner(Player) IsThreeInADiagonalWinner(Player) ) winner = true; return winner; private bool IsThreeInADiagonalWinner(player Player) bool winner = false; if (( this.board[(int)row.one, (int)model.column.one] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.three] == (int)player )) winner = true; if (( this.board[(int)row.one, (int)model.column.three] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.one] == (int)player )) winner = true;
TDD 예제 : 틱택토게임 131 return winner; private bool IsThreeInAColumnWinner(player Player) bool winner = false; for (int Column = 0; Column <= 2; Column++) if ((this.board[(int)row.one, Column] == (int)player && this.board[(int)row.two, Column] == (int)player && this.board[(int)row.three, Column] == (int)player)) winner = true; return winner; private bool IsThreeInARowWinner(player Player) bool winner = false; for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player && this.board[row, (int)column.two] == (int)player && this.board[row, (int)column.three] == (int)player)) winner = true; return winner; 마지막테스트이자게임에서구현해야할상태들중마지막상태는 모든공간이채워지고승자가없으면게임은무승부가된다 항목이다. [Test] public void When_All_Squares_Are_Full_And_There_Is_No_Winner_The_Game_Is_A_Draw() TicTacToeGame agameoftictactoe = new TicTacToeGame(); Row currentrow;
132 CHAPTER 4 테스트주도개발 for (int R = 0; R <= 2; R++) currentrow = (Row)Enum.ToObject(typeof(Row), R); agameoftictactoe.placemarkerat(currentrow, Column.One ); agameoftictactoe.placemarkerat(currentrow, Column.Three); agameoftictactoe.placemarkerat(currentrow, Column.Two ); // Game Board After All Moves // X X O // O O X // X X O Assert.AreEqual(GameStatus.GameDrawn, agameoftictactoe.status()); 모든공간이채워진상태에서승자가없다면게임은무승부가된다. 따라서승자를판단한이후에는모든공간이채워져있는지를검사하는코드를추가하면된다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public GameStatus Status() GameStatus GameStatus = Model.GameStatus. Draw; if (isawinner(player.o)) GameStatus = Model.GameStatus.PlayerOWins; else if (isawinner(player.x)) GameStatus = Model.GameStatus.PlayerXWins; else if (GameIsDrawn()) GameStatus = Model.GameStatus. Draw; else if (WhoseTurn() == player.x) GameStatus = Model.GameStatus.AwaitingPlayerXToPlaceMarker; else GameStatus = Model.GameStatus.AwaitingPlayerOToPlaceMarker; return GameStatus;
TDD 예제 : 틱택토게임 133 private bool GameIsADraw() bool allsquaresused = true; for (int Row = 0; Row <= 2; Row++) for (int Column = 0; Column <= 2; Column++) if (this.board[row, Column] == 0) allsquaresused = false; return allsquaresused;... 여러분이리스트에미처작성하지않은테스트중하나는누군가게임에승리한이후의처리이다. 이경우에는게임이더이상진행되지않도록하는것이옳다고할수있기때문에이런경우에대한테스트코드도작성해야한다. 이를위해 게임에서누군가승리한후에는더이상게임을진행할수없다 는이름의테스트메서드를추가해야한다. 이경우게임에서누군가승리한상태를만들어야하기때문에메서드추출리팩토링기법을이용해앞에서의테스트를위해작성했던코드를재사용해보자. using Microsoft.VisualStudio.TestTools.UnitTesting; using ProEnt.Chap4.TicTacToe.Model; namespace ProfessionalEnterprise.Chap4.MSTests [TestFicture] public class TicTacToeGameTests... [Test] public void If_Player_X_Gets_Three_Xs_In_A_Row_Then_The_Game_Is_Won_By_Player_X()
134 CHAPTER 4 테스트주도개발 TicTacToeGame agameoftictactoe = GetThreeXsInARowWinningGame(); Assert.AreEqual(GameStatus.PlayerXWins, agameoftictactoe.status()); private TicTacToeGame GetThreeXsInARowWinningGame() Row PlayerX_RowMove1 = Row.One; Row PlayerX_RowMove2 = Row.Two; Row PlayerX_RowMove3 = Row.Three; Column PlayerX_ColumnMove123 = Column.One; Row PlayerO_RowMove12 = Row.One; Column PlayerO_ColumnMove1 = Column.Two; Column PlayerO_ColumnMove2 = Column.Three; TicTacToeGame agameoftictactoe = new TicTacToeGame(); // Player X Move 1 // X // // agameoftictactoe.placemarkerat(playerx_rowmove1, PlayerX_ColumnMove123); // Player O Move 1 // X O // // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove1); // Player X Move 2 // X O // X // agameoftictactoe.placemarkerat(playerx_rowmove2, PlayerX_ColumnMove123); // Player O Move 2 // X O O // X // agameoftictactoe.placemarkerat(playero_rowmove12, PlayerO_ColumnMove2);
TDD 예제 : 틱택토게임 135 // Player X Move 3 // X O O // X // X agameoftictactoe.placemarkerat(playerx_rowmove3, PlayerX_ColumnMove123); return agameoftictactoe;... [Test] public void A_Player_Can_Make_No_More_Moves_After_A_Game_Is_Won() TicTacToeGame agameoftictactoe = GetThreeXsInARowWinningGame(); Assert.AreEqual(GameStatus.PlayerXWins, agameoftictactoe.status()); // 게임이종료되면빈공간이라도표시를할수없다. Assert.IsFalse(aGameOfTicTacToe.CanPlaceMarkerAt(Row.Three, Column.Three )); 현재코드에는게임이종료됐는지확인하는메서드가존재하지않기때문에이테스트는당연히실패하게된다. 테스트를통과하려면단순히게임에서누군가이겼거나혹은무승부인지게임의상태를검사하는코드를추가하면된다. 가장쉬운방법은현재게임에서플레이어중한명이표시를할차례인지를검사하는것이다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame... public bool CanPlaceMarkerAt(Row row, Column column) if (Status() == GameStatus.AwaitingPlayerOToPlaceMarker Status() == GameStatus.AwaitingPlayerXToPlaceMarker )
136 CHAPTER 4 테스트주도개발 return this.board[(int)row, (int)column] == 0; return false;... 이제여러분이작성했던모든테스트를통과했으므로이제이소프트웨어는기본적인요구사항을모두만족한다는것을보장할수있을뿐아니라코드를변경하거나리팩토링하더라도그에따른영향은없는지를즉시판단할수있게되었다. 또한이테스트코드는틱택토게임의규칙을이해하기위한용도로도활용할수있으므로향후에다른개발자도여러분의게임애플리케이션에구현된로직을쉽게이해할수있게된다. 틱택토게임의전체소스는다음과같다. namespace ProEnt.Chap4.TicTacToe.Model public class TicTacToeGame private int[,] Board = new int[3, 3]; private player currentplayer; public TicTacToeGame() currentplayer = player.x; public player WhoseTurn() return currentplayer; public bool CanPlaceMarkerAt(Row row, Column column) if (Status() == GameStatus.AwaitingPlayerOToPlaceMarker Status() == GameStatus.AwaitingPlayerXToPlaceMarker ) return this.board[(int)row, (int)column] == 0; return false;
TDD 예제 : 틱택토게임 137 public void PlaceMarkerAt(Row row, Column column) if (CanPlaceMarkerAt(row, column)) this.board[(int) row, (int) column] = (int) WhoseTurn(); ChangeCurrentPlayer(); else throw new ApplicationException( string.format("square Row:0, Column:1 already occupied by 2", row.tostring(), column.tostring(), (player)enum.toobject(typeof(player), this.board[(int)row, (int)column]))); private void ChangeCurrentPlayer() if (this.currentplayer == player.x) this.currentplayer = player.o; else this.currentplayer = player.x; public GameStatus Status() GameStatus GameStatus = Model.GameStatus.Draw; if (IsAWinner(player.o)) GameStatus = Model.GameStatus.PlayerOWins; else if (IsAWinner(player.x)) GameStatus = Model.GameStatus.PlayerXWins; else if (GameIsADraw()) GameStatus = Model.GameStatus.Draw; else if (WhoseTurn() == player.x) GameStatus = Model.GameStatus.AwaitingPlayerXToPlaceMarker; else
138 CHAPTER 4 테스트주도개발 GameStatus = Model.GameStatus.AwaitingPlayerOToPlaceMarker; return GameStatus; private bool GameIsADraw() bool allsquaresused = true; for (int Row = 0; Row <= 2; Row++) for (int Column = 0; Column <= 2; Column++) if (this.board[row, Column] == 0) allsquaresused = false; return allsquaresused; private bool IsAWinner(player Player) bool winner = false; if (IsThreeInARowWinner(Player) IsThreeInAColumnWinner(Player) IsThreeInADiagonalWinner(Player) ) winner = true; return winner; private bool IsThreeInADiagonalWinner(player Player) bool winner = false; if (( this.board[(int)row.one, (int)model.column.one] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.three] == (int)player ))
TDD 예제 : 틱택토게임 139 winner = true; if (( this.board[(int)row.one, (int)model.column.three] == (int)player && this.board[(int)row.two, (int)model.column.two] == (int)player && this.board[(int)row.three, (int)model.column.one] == (int)player )) winner = true; return winner; private bool IsThreeInAColumnWinner(player Player) bool winner = false; for (int Column = 0; Column <= 2; Column++) if ((this.board[(int)row.one, Column] == (int)player && this.board[(int)row.two, Column] == (int)player && this.board[(int)row.three, Column] == (int)player)) winner = true; return winner; private bool IsThreeInARowWinner(player Player) bool winner = false; for (int Row = 0; Row <= 2; Row++) if ((this.board[row, (int)column.one] == (int)player && this.board[row, (int)column.two] == (int)player && this.board[row, (int)column.three] == (int)player)) winner = true; return winner;
140 CHAPTER 4 테스트주도개발 지금까지의내용을통해테스트주도개발이어떤것인지에대해학습해보았다. 전체적으로코드의디자인이뛰어나다고는할수없지만최소한의코드로전체게임을구현할수있었으며, 테스트를통과하기위한코드만을구현하였다. 더좋았던점은모든테스트가완료되고더이상테스트할기능이없게되는시점에여러분이해야할작업이완료되었음을알수있게되었다는점이다. 프로그램을구현하기위한코드를먼저작성했다면코드가어떻게구현되었을지생각해보자. 아마도정확한규칙을이해하지못한채보드객체나플레이어객체를먼저구현했을것이다. 그러다보면프로그램의요구사항과맞닥뜨리게되고, 마지막에는코드가전체적으로동작하는지를확인하기위해단위테스트를추가하든가혹은그렇게하지도못하고일을마무리할수도있었을것이다. 테스트코드를먼저작성하면서애플리케이션의디자인을구성했기때문에우선최소한의노력으로요구사항을만족하는코드를만들어낼수있다. 가장좋은점은한두가지메서드가자신의역할이아닌다른역할을해야하거나게임의승자를검사하는로직이다른클래스에작성되어야한다면애플리케이션의디자인을손쉽게리팩토링할수있으며, 이런리팩토링과정이 TicTacToeGame 클래스의다른동작에어떤영향을미치지는않는지를테스트코드들을통해곧바로확인할수있다는점이다. 테스트프레임워크 지금까지의예제에서는테스트프레임워크로 NUnit 프레임워크를사용했지만 NUnit 프레임워크가유일한테스트프레임워크는아니며, NUnit 애플리케이션을실행하는것이테스트코드를실행하는유일한방법도아니다. NUnit 틱택토게임을구현하는동안우리는 NUnit 프레임워크를사용했다. NUnit은자바환경에서사용하던 JUnit을.NET으로포팅한오픈소스제품이며,.NET 환경에서도널리사용되고있다. NUnit에대한보다자세한내용은 http://www.nunit.org를참고하기바란다. MS Test NUnit과동일한기능을제공하는마이크로소프트의통합테스트도구로문법에는다소차이가있지만 Visual Studio에통합되어제공된다.
TDD 예제 : 틱택토게임 141 MS Test 프레임워크는 Visual Studio 2008 Professional 혹은그이상의버전에서제공된다. MbUnit MbUnit은또다른오픈소스테스트프레임워크이다. 이제품은다양한기능을제공하며손쉽게확장이가능하다. 만일 NUnit이나 MSTest 프레임워크에한계를느낀다면 MbUnit을필요에따라확장하여사용할수도있다. MbUnit에대한보다자세한내용은 http://mbunit.com을참고하기바란다. TestDriven.NET TestDriven.NET 자체는테스트프레임워크가아니지만 Visual Studio에서테스트프레임워크를선택할수있도록해주는애드인 add-in 이다. 이도구를이용하면 NUnit, MbUnit 및 MSTest 프레임워크는물론다른모든오픈소스테스트프레임워크를통합할수있어테스트주도개발에대한경험을향상시킬수있다. 개인용버전은무료이며, 보다자세한내용은 http://www.testdriven.net을참고하기바란다. 테스트가가능한요소들을정의하기 이제여러분은 TDD 개발방법론과절차에대해이해하게되었을것이므로이제는어떤부분에대해테스트를진행해야하는지를고민해보자. 100% 의코드커버리지 code coverage 를갖춘다는것은완벽한단위테스트집합을갖춘다는뜻은아니다. 이런생각은품질보다는단위테스트의개수에집착하는것일뿐이다. 개발방법론의규칙과이를비즈니스의요구사항에실용적으로반영하는것사이에는항상타협이필요하다. 이번섹션에서는단위테스트에대해여러분이해야할것과하지말아야할것에대해자세히알아보도록하자. 소소한코드까지테스트하지말라 TDD의소개단계에서이미읽어보았겠지만 TDD는여러분이작성하는코드모듈의동작을검증하는단위테스트를작성함으로써여러분의코드를위한디자인을정립해나가기위한것이다. 따라서여러분이작성할테스트코드는기본적으로고수준의추상화를필요로한다. 따라서테스트를수행할때는세부적인구현보다는고수준의요구사항에대해생각하는것이좋다. 이런고수준테스트를진행하다보면세부적인객체들이필요해질때도있지만, 세부적인객체들에대한단위테스트가항상장점만을가지는것은아니다. 전자상거래서비스에서사용가능한모든통화단위를리턴하는서비스를검증하기위한테스트를예로들어보자. 아무런동작도수행하지
142 CHAPTER 4 테스트주도개발 않으며, 단지데이터전송객체 DTO: Data Transfer Object 로사용되는현재의통화단위객체에값을대입하거나가져오는속성에대한테스트코드를작성하고싶지는않을것이다. 단지속성만을가지며동작은수행하지않는간단한객체에대해단위테스트를수행하는것은아무런의미가없다. 이런객체들을테스트하는것은단위테스트코드에아무런가치를부여하지않으며, 최악의경우테스트코드들을어지럽힐뿐아니라더가치있는단위테스트들을발견하지못하게한다. TDD 프로세스를글자그대로해석하는것과실제시스템이필요로하는개별적이며단위테스트가필요한부분들을찾아내는것사이에는타협이필요하다. 사실단위테스트는뭔가잘못되어가고있거나혹은나중에변경될우려가있는부분을대상으로해야한다. 모든단일시나리오에대해테스트코드를작성한다면아무런가치도없는수십만개의테스트코드를작성하게될것이다. 서드파티모듈에대해테스트하지말라여러분이작성하지말아야할또다른단위테스트들은.NET 프레임워크자체나서드파티 API 들을테스트하기위한것들이다. 여러분이참조하는서드파티도구들을믿지못하거나혹은믿지못할것처럼보인다면이런도구들은사용하지말아야한다. 다시말하지만여러분이스스로판단해야한다. 참조되는어셈블리가시스템이핵심기능을제공하며비즈니스에민감한부분이라면그도구가필요로하는기능을제공하는지를명확히해야한다. 다시말해도구를의심하게되는순간단위테스트코드를작성하게된다. 적정한추상화수준에서테스트를수행하라단위테스트의목적은애플리케이션의실행을방해할수있는로직의전체적인위험을줄이기위한것이므로여러분이작성하는테스트코드가소프트웨어를구성하는고수준의추상화된모듈들을대상으로하는것이중요하다. 저수준의상세한코드들을맹목적으로테스트하는것은매우지루하고하기싫은작업이될뿐이며, 결국모든테스트코드를함께검증하는과정을거스르게할수도있다. 비즈니스가중점을두고있는부분들을검증하기위해서단위테스트를작성하는것만이최선의방법은아니다. 개발자들은코드로직에대한최상의검증이단위테스트를통해이루어질것인지아니면실제동작하는것을눈으로확인해야할것인지를항상최선의판단과경험을토대로결정해야한다.
TDD 예제 : 틱택토게임 143 경계조건에대한테스트순수주의자들은코드를완벽하게테스트하려면모든경계조건을테스트해야한다고말한다. 경계조건을테스트한다는것은테스트코드에서여러가지극단적인값들을사용하는것을의미한다. 만일오늘이후의날짜를전달받는메서드에과거의날짜나오늘날짜가전달되면이를어떻게처리할것인가? 다시말하자면, 경계조건을테스트할수는있지만반드시그렇게해야할까? 소프트웨어개발과관련된이슈들에대한여러가지답변들과마찬가지로이는필요에따라다르게대처할수있다. 즉, 뭔가잘못된경우어떤일이발생할것인지에따라다르다는것이다. 만일테스트중인부분이비즈니스에민감한부분이라면다양한경계조건에대해테스트를해야한다. 그렇지않다면기껏해야 InvalidArgumentException 예외가발생할뿐이며, 그런다고세상이끝나는것도아니므로개발자가스스로판단해야한다. 무엇을테스트하고무엇을테스트하지말아야할지판단하는것은여러분의상식을활용하는것이다. 상식은경험을통해더나아질수있다. 각각의코드에단위테스트가필요한지그리고 100% 의코드커버리지가실제로듣기만큼좋은것인지를가늠해야한다. TDD 방법론을글자그대로받아들이는것과실제적용에있어서는항상타협이필요한만큼 TDD에대한경험을쌓아갈수록작업을빨리마무리하기위한지름길을알게될것이다. 앞서언급했듯이시간이흐름에따라여러분과팀의다른구성원들이해야할일을어렵지않게결정할수있게되면이러한타협을해야할때가올것이다. 궁극적으로별다른방법이없고여러분이아직의심에서벗어나지못했다면우선테스트코드를작성하라. 나중에얼마든지제거할수있다. 이제여러분이어떤것을테스트하느냐에따라여러분이작성한테스트코드가효과적이며도움이될것인지그렇지않을것인지를충분히알수있게되었으리라믿는다. 유용한단위테스트작성하기 좋은단위테스트의조건은무엇일까? 여러분이작성한단위테스트가효과적이며, 실제로동작함은물론리팩토링시에진정한가치를제공해줄수있는지를판단하기위한항목은다음과같다. 테스트는빠르게실행되어야한다. 테스트는반드시자동화되어실행할수있어야한다. 테스트는원자성을가져야한다. 테스트는반복수행할수있어야한다.
144 CHAPTER 4 테스트주도개발 테스트는명시적이며가독성이뛰어나야한다. 테스트는독립적이어야한다. 테스트는손쉽게설정할수있어야한다. 테스트는모든면을고려해야한다. 테스트는빠르게실행되어야한다쉽게말해테스트코드가느리게동작한다면여러분은물론다른누구라도테스트를빈번하게수행하지않을것이며, 따라서테스트의활용도가떨어지게된다. 테스트의실행속도가느리면귀한시간을테스트를실행하느라허비하게되므로테스트는반드시빠르게실행되어야한다. 이로인해여러분의작업주기를빠르게가져갈수있으며, 기능을추가하거나코드를리팩토링했을때버그를만들어내지않았음을확인할수있게된다. 테스트코드가자연스럽고빠르게실행되도록하는유일한방법은가상객체를이용하여시스템외부로부터의의존성을제거하는것이다. 만일단위테스트코드가데이터베이스에연결을시도하거나웹서비스를호출한다면테스트가메모리에서실행될때보다훨씬느리게동작할것이다. 의존성을가진인터페이스에대해가상객체를사용하면테스트코드를독립적으로실행할수있으며, 항상일관된결과를가져온다는것을보장할수있다. 가상객체에대해서는이장의후반부에서더자세히살펴보기로하자. 데이터베이스연결을수행하는통합테스트를작성할때에도이런코드를별도의어셈블리에분리하여구현함으로써테스트코드가빠르게동작하도록구현할수있다. 많은소프트웨어제조사들은소스코드를소스제어저장소에체크인하기전에전체단위테스트를실행하는데, 이런경우테스트코드가느리게동작하면개발자들이하루일과를마무리하면서자신들이작성한코드를검증하는것을꺼리게되므로테스트코드가빨리동작하는것이매우중요하다. 테스트는반드시자동화되어실행할수있어야한다코드의변경사항이미치는영향에대해빠르게판단할수있으려면여러분이나팀의구성원들이버튼하나만클릭해도테스트코드가동작할수있어야한다. 만일테스트코드를실행하는것자체가쉽지않거나별도의설정이필요하다면개발자들은테스트코드를실행하는것을좋아하지않을것이다. 간단히말해개발자들은소스제어저장소로부터최신버전의소스코드를내려받으면아무런추가설정없이도테스트코드를실행할수있어야한다. 테스트코드의실행이자동화되어손쉽게실행할수있다면개발자들이코드를업데이트할때도움이된다. 테스트가설정문제로실패하게되면개발자들은테스트코드에대해신뢰하지않
TDD 예제 : 틱택토게임 145 게되어테스트코드로서의가치가떨어지게된다. 따라서언제라도테스트를실행할수있는지에대해항상확인해야한다. 테스트는원자성을가져야한다여러분은테스트코드를몇번을실행하든항상같은결과를얻을수있어야한다. 따라서여러분이제어할수없는시간이나날짜, 데이터베이스의상태등을이용하는변수에의존해서는안된다. 대신항상일관된매개변수를이용하여테스트코드를언제몇번이라도실행할수있어야한다. 또한테스트코드를실행하기전에상태를가지고있던모든의존자원들은테스트가완료된이후원래상태로되돌아오는지를확인해야한다. 이경우셋업및해제이벤트를통해테스트가실행되기전의설정과실행이후의정리작업을수행할수있다. 이렇게하면항상깨끗한상태에서테스트가실행될수있다. 테스트는명시적이며가독성이뛰어나야한다테스트코드의집합은여러분의시스템이무엇을할수있고무엇을할수없는지를알려주는살아있는문서와도같은역할을하기때문에, 테스트코드에의미있고서술적인이름을부여하여다른개발자들이해당테스트가어떤동작을검증하기위한것인지를즉시이해할수있도록하는것이좋다. 테스트코드의이름은작성자의의도를알수있도록작성해야할뿐아니라명확하며이해가쉬워야한다. 여러분이작성하는테스트코드는프로그램의인터페이스를어떻게사용할것인지를보여주기때문에테스트코드의가독성은매우중요하다. 테스트코드에서중복된테스트를제거하고오래된테스트코드를삭제하여마치릴리즈할코드처럼전문적으로작성함으로써깔끔하고읽기쉬운상태를유지한다면, 이테스트코드는여러분에게더큰유용함과가치를가져다줄것이다. 테스트는독립적이어야한다테스트코드는반드시다른테스트코드로부터독립적이어야하며다른테스트코드에의존적이어서는안된다. 테스트가순서없이뒤죽박죽으로실행되는상황을가정한다면나중에실행될테스트는앞서실행된테스트에서설정한객체의상태에의존적이어서는안된다. 어떤하나의테스트코드의성공여부가다른테스트코드에영향을주어서는안된다. 객체의상태를변경하거나외부리소스를생성, 제거혹은기록하는테스트는상태해제메서드를이용하여테스트가완료된이후객체의원래상태를복원해야한다.
146 CHAPTER 4 테스트주도개발 테스트는손쉽게설정할수있어야한다테스트코드를설정하느라오랜시간을보낸덕분에실제릴리즈할코드를작성할시간이부족하다면뭔가잘못된것이다. 테스트코드는직관적이어야하며손쉽게설정이가능해야한다. 만일설정을위한코드를작성하느라시간을허비했다면여러분이테스트하려는코드를의미있는섹션에서분리하거나동일한객체의동일한상태를필요로하는다른테스트코드와공유할수있는설정루틴을만드는것이좋다. 테스트커버리지와다각도테스트하나의테스트에통과했다고해서로직자체가올바르게동작한다고보장할수는없다. 앞서작성한틱택토게임에서보았듯이여러분은테스트를통과하기위한최소한의코드만을작성했다. 그러나간혹이코드는다른테스트시나리오에서는올바르게동작하지않을수도있음을알게되었다. 여러분은 99% 의경우테스트를통과할수있는일반적인인수는물론극단적인경계값을이용하여테스트를수행할필요가있다. 요약하자면테스트코드도실제릴리즈할코드처럼전문적인코드로취급하면테스트코드는그만큼의보답을할것이다. 테스트코드는여러분이프로젝트에너무깊숙이빠져들때경고를해주는안전망을제공할것이다. 더이상테스트코드가필요없게되면이를제거하여전체적으로테스트코드가의미없는코드들덕분에어지러워지는것을방지해야한다. 테스트코드의이름은작성자의의도를충분히표현할수있도록가능한서술적으로작성해야한다. 기억할것은테스트코드가실제로릴리즈될코드가아니라는것이며, 여러분의프로그램이할수있는것과그렇지않은것을표현하기위한것이므로테스트코드의이름이명확할수록여러분과다른개발자들에게더가치있는것이된다. 마지막으로테스트코드를손쉽게설정하여실행할수있도록해야한다. 설정이어렵거나느리게실행되는테스트는여러분과다른개발자들이테스트코드를실행하는것을기피하게만들뿐이다. 그렇게되면버그가양산될것이며형편없는품질의코드가만들어질것이다. 다시말하지만일상생활과마찬가지로여러분스스로가판단해야한다. 테스트코드를설정하는데많은시간이필요하다면여러분은실제릴리즈코드를구현하기위해더많은시간이필요하게되므로차라리프로그램의기능을수동으로테스트하거나보다작은단위로나누는것이더효율적이다. 지금까지여러분은 TDD가무엇이며, 이개념을어떻게적용해야하는지, 어떤것을테스트해야하는지, 그리고테스트코드들을어떻게하면더효율적으로구성할수있는지에대해학습했다. 이제는 TDD를따랐을때얻을수있는현실적인이점이무엇이며, 여러분의투자가어떤결실을맺을수있는지에대해알아보자.
TDD 예제 : 틱택토게임 147 자동화 - 실질적인이점 TDD를통해얻을수있는가장첫번째이점중하나는프로그램의구현을완료하면여러분의기반코드를구성하는각각의로직을위한테스트코드들을얻을수있다는것이다. 이수많은테스트들은마치보험약관같은것이어서나중에어떤형태로코드가변경되더라도그영향을곧바로알수있으며, 만일변경사항이기존의모듈에버그를유발시키게된다면그즉시알수있게된다. TDD 방법론을따름으로써이룰수있는것은소프트웨어내의위험을줄을수있다는것이다. 버튼만클릭하면여러분의소프트웨어가요구대로동작하는지를언제든확인할수있으며, 더욱중요한것은다른개발자가대량의코드를변경한이후에자신만의방법으로디버그를수행할필요없이이테스트코드들을실행해보면된다는것이다. 단위테스트를자동화함으로써얻을수있는현실적인이점은수동테스트에대한필요성을감소시킬수있다는것이다. 주요테스트코드들을자동화함으로써인력의간섭을줄일수있다. 개발자에게모든코드시나리오에대해수동으로모든테스트와디버깅을진행하라는무자비한일을시키는것보다는테스트프레임워크가 1,000개의테스트를실행하도록하는것이훨씬쉽다. 두사람의개발자가똑같을수는없다. 누군가는다른사람보다테스트를더잘할수있으므로코드에변경이일어났다고해서개발자들이모든로직을테스트하는것보다는테스트프레임워크에게맡긴다면버그의출현을더확실하게검출할수있다. 또한소프트웨어의다음기능을구현하기위한시간도확보할수있다는이점이있다. 정의에따르면, TDD 방법론을따를경우여러분은테스트가가능한코드를만들어낼수있다. 테스트가능한코드는디자인적으로느슨하게결합되어있으며모듈화된코드를말한다. 각각의기능들을단일책임을가진클래스로분리하면유지보수와확장성에대한요구사항을줄일수있어비즈니스요구사항의변경에대응하기위한코드의유연성을향상시킬수있다. 이번장의후반부에서는실제코드가가지는의존성을가상객체로대체함으로써코드모듈을느슨하게결합된형태로유지할수있는방법에대해학습하게될것이다. 지금까지의내용들은코드를리팩토링하거나디자인을결정할때개발자인여러분들에게확신을안겨줄수있을것이다. 테스트코드를통해서여러분의코드가올바르게동작하는지여부를곧바로알수가있다. 이번장의후반부에서는디자인패턴을이용하여코드를보다단순하게디자인할수있는방법에대해학습한다. 개요부에서읽었듯이 TDD는여러분이작성한코드가올바르게동작한다는것을증명할뿐아니라소프트웨어의디자인에도중요한영향을미친다. TDD는실제릴리즈할코드를작성하기에앞서여러분이기대하는결과를먼저테스트함으로써여러분이해결하고자하는문제에대해
148 CHAPTER 4 테스트주도개발 완벽하게이해하도록만든다. 즉, 여러분이소프트웨어의요구사항을분명하게이해하는것뿐아니라최종 API를구현할수있도록해준다. 테스트코드를먼저디자인함으로써여러분은인터페이스가어떻게동작할것인지를구상할필요가있다. 또한여러분이구현한모듈을실제로사용할사람의관점에서테스트코드를작성하게되며, 코드자체의기능보다는코드의인터페이스에더초점을맞추게된다. 그러려면기능이동작하도록하기전에클라이언트 ( 테스트 ) 코드와 API 사이의상호작용에대해생각해야하며, 그로인해사용이쉬우면서도다른개발자가의도를쉽게파악할수있는, 간단하면서도유연한 API를작성하게된다. 여러분이작성한테스트코드는소프트웨어가할수있는일과할수없는일의목록이된다. 또한 API와어떻게상호작용할수있는지를보여주는일련의예제코드로서의역할도수행할수있다. 따라서여러분이작성한코드가계속해서발전하는데있어살아숨쉬는문서의역할을수행할수있다. 바로이런점때문에테스트코드의이름을지정하는것과테스트코드자체를깔끔하게유지하는것이중요하다. 틱택토예제를위해여러분이구현했던테스트코드를읽어보면게임자체와그규칙에대해알아야할모든것들을배울수있다. 무엇을테스트해야할지를알아보았던이전섹션에서지적했듯이여러분이작성하는초기의테스트코드는모듈의구체적인구현보다는추상화에초점을맞춘다. 이렇게하면개발자는모듈의실제비즈니스적가치에초점을맞추게되며, 그후코드들을검증하고에러처리나유효성검사와같은경계사항들을테스트하는데사용할수있게된다. 이런과정을통해실제로동작하는핵심기능들을구축할수있고, 이를토대로여러분의릴리즈코드를개발해나갈수있게된다. 이때작성되는코드들은테스트에통과하기위한목적으로작성되기때문에여러분은 YAGNI (You Aren't Gonna Need It) 디자인패턴을활용하는셈이다 ( 옮긴이 _YAGNI 패턴이란, 주로익스트림프로그래밍방법론에등장하는것으로실제로필요해지기전까지는어떤코드도추가하지말것을권장하는프로그래밍원칙이다 ). 프로그램이필요로하는코드만을작성하고테스트에실패하는경우에도같은방식으로테스트를통과하는코드를작성하게된다면여러분은모든경우의코드를검증할수있는테스트코드집합을갖추게된다. 최소한의코드만으로이와같은테스트코드를작성한다면경우에따라프로젝트의불필요한복잡성을줄일수있다. 반드시필요한코드만을작성하면여러분과팀의생산성을향상시킬수있다. 잘갖춰진단위테스트코드들은한사람의개발자에대한의존성을줄일수도있다. 이러한의존성은주로트럭팩터 truck factor 라고부른다. 트럭팩터란, 팀의몇몇구성원이실제프로젝트에서심각한문제가발생하기전까지는대부분의업무를처리하게되는상황을의미한다. 테스트코드를작성하면새로운개발자들이기존의코드를망치게될경우테스트코드가이를바로알
리팩토링 149 수있게해주기때문에새로운개발자들이손쉽게프로젝트에적응할수있게되며, 이미존재하는기반코드를빠르게학습할수있게된다. 또한실제로릴리즈될코드가어떻게작성되어야하는지를보여주기때문에새로운개발자에게더큰가치를제공하게된다. 이는 TDD의특징인유기적인문서와관련이있다. 요약하자면, TDD는여러분이소프트웨어를개발하는데있어곧바로활용할수있는다양하고실용적인이점을제공한다. 테스트를자동화하고실제코드를작성하기에앞서테스트코드를작성함으로써개발시에짊어질스트레스를날려버릴수있다. 또한여러분이작성한코드가동작한다는것과실제로작업을완료하기위한코드외에불필요한코드는존재하지않음을보장할수있다. 동일한기반코드로작업하는다른개발자들은자신들이변경하거나기능을추가할때테스트코드를통해기존코드의동작을검증할수있기때문에자신들의변경사항이기존코드의동작을방해하지않는다는것을보장할수있다. 짧은주기로작은단계의작업을수행하기때문에소프트웨어개발자체를통제할수있으며, 느슨하게결합된확장및유지보수가용이한고품질의코드를작성할수있게된다. 마지막으로, TDD의장점을모두취한다면여러분스스로가여러분의코드를신뢰할수있게된다는것이가장중요하다. 코드를수정하거나새로운기능을추가했을때즉각적인결과를알려줄수있는테스트코드들은여러분과팀에큰도움이될것이다. 또한여러분이작성한코드를신뢰할수있다면실제로코드를릴리즈할때나코드를수정하거나기능을추가해야할때더욱확신을갖게될것이다. 이번장의첫번째파트에서는여러분의코드가잘동작하는것은물론깔끔하게유지하는것이중요하다는것을알게되었다. 리팩토링이라고알려진코드정리작업은테스트코드를작성하는것만큼중요한주제이다. 리팩토링 제3장에서여러분은테스트주도개발의 레드- 그린- 리팩토링 단계에서세번째단계인리팩토링에대해간단히살펴본적이있다. 리팩토링이란, 기존의코드디자인을향상시키기위한방법이다. 틱택토게임예제에서는모든테스트가성공적으로수행되어코드가올바르게동작하게된후에는코드에대한리팩토링을수행하여코드자체의동작을변경하지않고도코드를더욱깔끔하고유지보수가용이하도록구성할수있는단계에이르렀었다. 간단히말하면, 사실그다
150 CHAPTER 4 테스트주도개발 지간단한일은아니지만코드는항상손쉽게변경이가능한구조를유지해야한다. 그렇다면왜코드를리팩토링해야하는걸까? 뭔가잘못되지않았다면고칠필요가없다 라고보는것이올바르지않을까? 리팩토링은코드의버그를수정하는것이아니며, 다른기능을추가하는것도아닌데대체왜해야하는걸까? 리팩토링의진정한가치는코드를보다이해하기쉽게하며, 여러분과다른개발자들이코드를유지보수하기쉽도록하는데있다. 마틴파울러 Martin Fowler 는리팩토링에대해다음과같이훌륭하게요약하고있다. 컴퓨터가이해할수있는코드는바보라도작성할수있다. 훌륭한프로그래머는인간이이해할수있는코드를작성한다. ( 마틴파울러, Refactoring, 1999년 Addison-Wesley Object Technology 시리즈에서 ) 코드를적절하게리팩토링하지않으면새로운기능을추가하거나기존기능을수정해야할때곧바로문제점으로나타날수있다. 코드를깔끔하게리팩토링하지않으면결국에는스파게티처럼뒤얽힌구조로인해작업이불가능하며유지보수가어려운상태가되고만다. 리팩토링을수행하는입장에서여러분은여러분이작성한코드를더욱잘이해할수있게되며, 따라서이를더간단하며더나은구조로리팩토링할수있다. 또한다른개발자들이여러분이작성한코드를더쉽게이해할수있도록잘알려진디자인패턴을이용하여리팩토링할수도있다. 리팩토링은 TDD의영역에만국한된것은아니지만, TDD와함께수행하면여러분이수행한리팩토링의영향을손쉽게파악할수있어여러분이변경한사항이모듈의동작에영향을미치지않는다는것을곧바로확인할수있다. 리팩토링을수행할때기억해야할가장중요한것은리팩토링은작은단계로진행되어야하며, 각리팩토링이완료된후에는반드시테스트코드를실행해야한다는것이다. TDD 방법론에따라짧은주기로작업을진행한다면버그의출현으로인한위험을감소시킬수있으며, 기존의테스트를통과하지못하는부분을손쉽게찾아낼수있다. 리팩토링은 TDD와밀접한관계가있다. 여러분이작성한테스트코드덕분에보다확신을가지고리팩토링을수행할수있으며, 코드리팩토링은코드를더욱간단명료하게하여더욱테스트하기쉽게만든다.
테스트주도개발환경에서의존성처리하기 151 리팩토링도구 테스트프레임워크와함께리팩토링에사용할수있는도구들에대해알아보자. ReSharper ReShaprer는 Visual Studio의애드인으로몇가지향상된기능을제공하는동시에강력한리팩토링기능을제공한다. 또한 NUnit 프레임워크와통합되어한번의클릭으로통합된단위테스트프레임워크를실행할수있다. 보다자세한내용은 http://www.jetbrains.com/resharper를참고하기바란다. Refactor Pro Refactor Pro는 ReSharper의모든기능을제공하지만사용자인터페이스를위한통합된테스트기능이제외되어있다. 어떤것을선택할지는개인의취향에따라다르겠지만둘다훌륭한제품임에는틀림이없다. 보다자세한내용은 http://www.devexpress.com/products/visual_studio_add-in/refactoring/ 을참고하기바란다. 테스트주도개발환경에서의존성처리하기 테스트코드를작성할때의핵심특성중하나는각기능에대한테스트가독립적으로이루어진다는것이다. 그러나제3장의 ReturnOrderService 클래스에서살펴보았듯이프로그래밍의강력함은복잡한문제를해결하기위해객체들이협력할때발휘된다. ReturnOrderService 클래스의 Process 메서드는고객에대한환불처리를 IPaymentGateway 인터페이스를구현한객체에게위임한다. 또한고객에게환불사실을알리기위한처리도 INotificationService 인터페이스를구현한객체에게위임한다. 다른모듈에의존적인로직을테스트할때간혹다른모듈들을포함해전체통합테스트를해야할때가있다. 특히존재하지않거나혹은의존객체가아직구현되지않은상황에서테스트를수행해야한다면상황은더욱악화될뿐이다.
152 CHAPTER 4 테스트주도개발 의존성을가진객체를위한테스트코드를작성해야한다면우선은실제객체의역할을대행할스텁 stub 객체 ( 혹은가상객체 ) 를구현할수있다. 이스텁객체의역할은테스트중인객체가필요로하는데이터를흉내내는것이다. 스텁객체는일반적으로단위테스트의실패와는무관하다. 스텁객체는단순히테스트중인객체가작업을완료하기위해필요로하는데이터를제공하는목적으로사용된다. 스텁객체를이용하면데이터베이스나웹서비스를호출하는서비스객체를실제구현과동일하게동작하는가상객체로대체함으로써의존성을손쉽게제거할수있어테스트를일관적이고빠르게수행할수있다. 스텁객체나가상객체를사용하는방법을연습하기위해휴가요청예약시스템을구현해보자. 그림 4-9는이시스템을구성하는클래스들을보여준다. HolidayRequest Class Fields Properties Approved From To IHolidayRequestValidator Interface Methods CanGrant HolidayRequestService Class Methods HolidayRequestService Submit IHolidayRequestReposityory Interface Methods Save 그림 4-9 테스트코드를작성하기에앞서그림 4-9에나타난클래스들을먼저구현해야한다. 1. Visual Studio를실행하고 ProEnt.Chap4.Mocking이라는이름의새솔루션을생성한다. 2. 솔루션탐색기에서솔루션항목을마우스오른쪽버튼으로클릭하고 [ 추가 새프로젝트 ] 메뉴를선택하여 ProEnt.Chap4.Mocking.Model이라는이름의클래스라이브러리프로젝트를추가한다. 3. 2번항목과같은방법으로 ProEnt.Chap4.Mocking.Model.Tests라는이름의두번째클래스라이브러리항목을추가한다.
테스트주도개발환경에서의존성처리하기 153 4. 모든프로젝트를생성했으면솔루션항목을마우스오른쪽버튼으로클릭하고, [Windows 탐색기에서폴더열기 ] 메뉴를클릭하여 Windows 탐색기를실행한다. 솔루션폴더가열리면 lib이라는이름의폴더를새로생성하고, Nunit.framework.dll 파일을 NUnit 디렉터리에서 ( 기본적으로 %systemdrive%\%programfiles directory%\nunit 2.4.8\bin 폴더에위치하고있다 ) 복사하여새로생성한 lib 폴더에붙여넣는다. 5. Visual Studio 의솔루션탐색기에서솔루션항목을마우스오른쪽버튼으로클릭하고, [ 추가 새솔루션폴더 ] 메뉴를선택하여솔루션에 lib 이라는이름의새솔루션폴더를추가한다. 6. lib 솔루션폴더를마우스오른쪽버튼으로클릭하고, [ 추가 기존항목추가 ] 메뉴를선택한후솔루션의루트에위치하는 lib 폴더를찾아 NUnit.framework.dll 파일을추가한다. 7. ProEnt.Chap4.Model.Tests 프로젝트를마우스오른쪽버튼으로클릭하고, [ 참조추가 ] 메뉴를선택하여 NUnit.framework.dll 파일에대한참조를추가한다. 참조추가대화상자가나타나면 [ 찾아보기 ] 탭을선택하고, Lib 폴더를탐색한후 NUnit.framework.dll 파일을선택한다. 8. ProEnt.Chap4.Mocking.Model.Tests 프로젝트에 ProEnt.Chap4.Mocking.Model 프로젝트에대한참조를추가한다. 9. ProEnt.Chap4.Mocking.Model 프로젝트에 HolidayRequset라는이름의새클래스를추가하고다음의코드를작성한다. 이클래스는휴가요청자체를표현한다. namespace ProEnt.Chap4.Mocking.Model public class HolidayRequest private DateTime _from; private DateTime _to; private string _employeeid; private bool _approved; public DateTime From get return _from; set _from = value; public DateTime To