U E D R , A S I H C RSS

신재동/Practice ByTDD


1. 서문

아직 생소한 TestDrivenDevelopment에 익숙해지기 위해서는 연습이 최고라 생각해서 시작합니다...;;; 우선은 전에 데블스캠프에서 한 쉬운 것부터 차근차근 다시 시작해보려합니다. 당연 부족한 점이 엄청 많습니다. 지적해주시고 가르쳐 주시면 감사하겠습니다.;;;

2. 구구단

5. Thread

  • TDD 실제로 사용한게 거의 첨이라 제대로 하는 지도 모르겠고 엄청 버벅댔습니다. 그리고 printDan() 함수 같은거는 어떻게 test 해야할지 잘모르겠습니다. 그냥 출력해서 보는 건 아닐꺼 같은데... 출력함수는 자동 테스팅을 어떻게... --재동
    • 보통 입출력 부분에 대해서까지 TDD 로 할 필요는 없음. 그리고 OOP 에서 Logic 에 해당하는 Object 가 직접 print 출력 함수를 가지는 경우자체가 없다고 보는게 맞을듯. Model 과 View 부분은 보통 중상 크기의 프로그램 디자인에선 전부 분리를 시키는게 추세. (MVC Model) 자네가 String Object 를 쓸때 String.print(); 이런 메소드가 없는것을 생각해보시길. 그렇지 않은 경우는, GOD Class 식으로 (클래스 하나가 모든 일을 다 하는. 해당 Class 의 Client 가 없는) 쓰인게 아닌지 생각해보는게 좋을것 같다.

      만일 View 에 로직이 있는 부분이 있다면 (ex: GUI Programming 에서 컨트롤간 상호관계 등) 그때 View 관련 로직에 대해서 TDD 를 시도하면 될듯. (GuiTesting 페이지 참조) --1002

만약 ~cpp Formatter라는 클래스가 존재한다면 어떨까요? 그러면 좀 더 안전감을, 확신을 갖게 되지 않을까요? 그렇게 하고도 좀 불안하다면, 맨 마지막 단계의 로우레벨 프린팅은:
  1. 인자로 ~cpp OutputStream을 전달받도록 코드를 고치고, 테스트시에는 ~cpp MockObject를 쓰면 어떨까요? ~cpp PrintStream을 상속받고 몇 가지 메쏘드를 오버라이드하면 되지 않을까요? ~cpp ByteArrayOutputStream을 사용하면 ~cpp MockObject를 안만들고도 할 수 있겠죠.
  2. 혹은 별도의 인자를 사용하지 않고, ~cpp System.setOut을 이용 리디렉션할 수도 있는데 이 경우 ~cpp tearDown 등에서 원상 복구해주어야 합니다. ~cpp LoD를 지키려 한다면 이 방법은 피하는 게 좋습니다. (나중에 구구단의 출력이 화일로 바뀌어야 하는 경우를 생각해 보세요 -- 1번 경우는 코드의 재사용성이 높아집니다)
어찌되건 시스템의 가장자리(사용자 UI, 네트워크, 화일 I/O 등)는 가능하면 얇게 만드는 것이 테스트가능성이나, 코드 디자인 등의 측면에서 바람직합니다. --JuNe

  • Python에서 동적 리스트는 어떻게 해야 할지... 위에 마방진 소스중 ~cpp createBoard() 함수처럼 반복문으로 일일이 추가를 해야되나요?? 못찾았지만 자바같이 setResize() 같은 함수가 있을듯도 한데... 한편 문득 생각난 건데 TDD와 TFP에 다른 점이 있나요?? 동일한 뜻 같은데 두개의 용어를 사용하는게 좀 의심스러워서...^^;;; 혹시나 다른 차이점이 있는지...??? --재동

~cpp NumPy 모듈이 매트릭스, 다차원 배열 처리에 무척 편리합니다. 위의 경우라면 저는 이렇게 코딩합니다.

~cpp 
>>> row,col=5,6
>>> board=[[None for i in range(col)] for j in range(row)]
>>> board
[[None, None, None, None, None, None], [None, None, None, None, None, None], [None, None, None, None, None, None], [None, None, None, None, None, None], [None, None, None, None, None, None]]

TDD와 TFP의 차이점이라면, TDD가 좀 더 넓은 개념이고, 공식 용어라고 생각하면 됩니다. KentBeck이 TFP란 말을 사용하다가 최근(올해)들어서 TDD로 대체했습니다. 이제는 TDD가 공식적인 명칭이고 TFP는 잘 쓰이지 않습니다. 사실 TDD로 대체한 이유 중 하나는 TFP에는 First란 말과 Programming이라는 말 등이 저차원의 구현 중심(implementation specific)이었다는 데 있습니다. 이에 비해 TDD는 좀 더 추상화된(abstract) 표현이지요 -- Driven에는 First가 포함되고, Development에는 Programming이 포함됩니다. 자바의 인터페이스와 임플리멘테이션의 차이라고 봐도 되겠죠.

그리고 마방진 코드는 좀 더 OOP적으로 추상화를 할 수 있겠습니다. 저라면 아래 코드 정도의 추상화가 나오도록 코딩을 할 것입니다.

~cpp 
class Counter
    ...
    def writeNextCell(...):
        self.move()
        self.write()
    def move(self):
        if self.isPlaceableOn("NE"):
            self.moveSafeToward("NE")
        else:
            self.moveSafeToward("S")
    def isPlaceableOn(self,aDirection):
        safeLocation=self.getSafeMove(aDirection)
        return self.board.isEmpty(safeLocation):
    def getSafeMove(self,aDirection):
        location=self.getPosition(aDirection)
        return self.board.getWrapAroundLocation(location)
    def moveSafeToward(self,aDirection):
        self.setLocation(self.getSafeMove(aDirection))

class Board
    ...

    def fill(...):
        for i in range(self.row*self.col):
            counter.writeNextCell(...)


지금 재동군이 TDD한 것의 테스트 코드를 보면 최상위 단계의 테스트(일종의 승인 테스트) 밖에 없는데, 그 정도로 확신(confidence)이 있고 자신이 있어서 큰 걸음을 한 것인가요? 저라면 위에 ~cpp getWrapAroundLocation, ~cpp isPlaceableOn 등도 테스트할 것입니다.

이런 추상적인 코드가 TDD로 잘 만들어지지 않는다면 Programming By Intention을 훈련해 보는 것도 좋습니다. PBI는 자신이 원하는 것을 컴퓨터에게 테스트 코드가 아니라 실행 코드로 설명을 하는 것입니다. 앞서의 예를 들자면 ~cpp writeNextCell이라는 메쏘드를 먼저 정의한 다음에 ~cpp move~cpp write 같은 메쏘드를 구현해 나가는 것이죠. StepwiseRefinement와 비슷하다고 볼 수 있습니다.

일단 내가 코딩을 하기만 하면 신기하게도 그 메쏘드들이 자동으로 다 구현이 된다고 상상을 하고 가장 추상적인 단위에서 코딩을 해보세요. 그리고 이걸 재귀적으로 행해보세요. 아주 신기한 경험을 하게 될 겁니다. (PBI에 대한 설명은 XPI 번역서를 참고하세요)
--JuNe

  • 창준 선배님의 말에 따라 함 고쳐보았습니다. 우선 의미가 불분명한 소스들을 ~cpp ExtractMethod를 했습니다. (그러다 보니 벽이 필요 없다는 알고리즘상 문제도 발견했다는...-,-;;;) 그후에 ~cpp ExtractClass를 했습니다. 물론 그것을 위해 테스트를 먼저 작성 했구요. 그리고 이번에는 전보다 테스트 보폭을 줄이려고 노력했습니다. 다 고치고 전과 후를 비교해보면서 여러가지가 느껴집니다. 특히 전에 짤때는 무심코 넘어갔는데 후에 고칠때는 코드에 냄새가 많이 나네요...^^;;; --재동

지금 보면 거의 모든 테스트가 ~cpp expectBoard와 연산된 결과를 비교하는 식인데 그렇게 하지 않고 더 낮은 차원에서 테스트할 수 있습니다. 예를 들어서 Counter의 ~cpp getPosition(aDirection)은 다음과 같이 테스트 프로그램을 먼저 작성했을 겁니다.

~cpp 
class CounterTest(unittest.TestCase):
    def setUp(self):
        self.c=Counter()
    def testGetPosition(self):
        self.c.setLocation(0,0)
        self.assertEquals((-1,1),self.c.getPosition("NE"))
        self.assertEquals((1,0),self.c.getPosition("S"))  #참고로 이 테스트 자체도 리팩토링 가능
        ...

--JuNe

  • 이번에는 RandomWalk2를 해보았습니다. 이동방향의 row와 col을 바꿔서 그것 찾는 데 엄청 삽질했다는...ㅠ,ㅠ 아주 당연시 한 것에서 오는 버그는 정말 찾기 힘들다는 걸 많이 느낀 코딩이였습니다. 그리고 이번에는 좀 무식하게RandomWalk2/TestCase의 테스트를 텍스트로 저장해서 다 해 보았습니다. 좀 쉬고 있다가 요구사항 2번째를 해야지요...^^;;; --재동
    "아주 당연시 한 것에서 오는 버그는 정말 찾기 힘들다"는 교훈을 얻는 정도에서 끝나면 다음에 같은 상황을 겪을 확률이 높습니다. 나를 고생시킨 버그를 발견하면, 설사 그 버그가 제거됐을지라도 그 버그를 발견해내는 테스트를 추가해 보는 것이 좋고, 다음번에는 아예 이런 버그로 고생하지 않으려면 테스트를 어떤 식으로 작성해야 할 지 "자기 행동 패턴에 대한 보정"을 고민해 봐야 합니다. 추상적인 차원의 교훈도 좋긴 하지만, 추상화는 구체화 이후에 따라와야 그 가치가 있습니다. 좀 더 구체적인 교훈을 생각해 보고, 가능하면 그걸 문서화, 정리해 두면 좋을 것입니다. 내가 어떤 잘못을 했고, 왜 그런 잘못을 했으며, 앞으로 어떻게 해야 그런 잘못을 안하거나 혹은 재빨리 알아챌 수 있게 할까 하는 걸 정리해 보면 좋겠죠. --JuNe

  • RandomWalk2 첫번째 요구 사항까지 해보았습니다. 요구사항만 통과하는 데 시간은 45분쯤 걸렸습니다. 후에 중복된 두개의 바퀴벌레를 리스트로 만들면서 또 한 20분쯤 걸렸습니다. 한편 이번에 잘못한 점이 하다보니 테스트의 보폭이 컸다는 걸 알았지만 그냥 했습니다. 물론 두 마리를 번갈아 가면서 움직이는 걸 해보긴 했지만(수동 테스트?) TDD에는 실패한듯합니다. 하지만 확실히 전에 테스트 해 놓았던 게 수정중 많이 도움이되었습니다. --재동
    보폭이 크면 상황이 어려워집니다. 그럴수록 사람들은 보폭을 줄이기보다 보폭을 더욱 늘리려고 합니다. 잘못된 걸 아는 것은 프로나 아마추어나 똑같습니다. 하지만, 잘못된 걸 하지 않는 것은 프로이고 하는 것은 아마추어입니다. --JuNe

  • 이번에는 RandomWalk2 두번째 요구 사항까지 해보았습니다. 요구사항 통과하는 데 20분 정도 걸렸습니다. 후에 리펙토링과 테스트를 추가하면서 40분 정도 걸렸습니다. 창준 선배님의 말씀대로 '당연시 한데서 오는 버그'나 'TDD 실패'는 작은 보폭으로 극복할 수 있다는 걸 알게되었습니다. 그래서 이번에는 테스트를 많이 늘렸습니다. 전에 작동한 걸 보았다 해도 테스트를 추가하여 더 확실히 했습니다. 테스트가 늘어나면서 자연히 보폭은 좁아졌습니다. 그리고 이번에 알게 된 건 리펙토링에는 TDD가 필수라는 걸 새삼 느꼈습니다. 변수 이동과 함수 이동시 테스트가 아주 자세히 라인(Error Line)을 알려줘서 바로 바로 고쳤습니다. 다음 요구사항에서는 작은 보폭으로 코딩을 하겠습니다...^^;;; --재동

Valid XHTML 1.0! Valid CSS! powered by MoniWiki
last modified 2009-05-27 07:09:19
Processing time 0.1900 sec