Toggle menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

신재동/PracticeByTDD

From ZeroWiki

서문

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

구구단

GuGuDan/재동

마방진

MagicSquare/재동

RandomWalk2

RandomWalk2/재동

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

만약 Formatter 라는 클래스가 존재한다면 어떨까요? 그러면 좀 더 안전감을, 확신을 갖게 되지 않을까요? 그렇게 하고도 좀 불안하다면, 맨 마지막 단계의 로우레벨 프린팅은:

  1. 인자로

OutputStream 을 전달받도록 코드를 고치고, 테스트시에는 MockObject를 쓰면 어떨까요? PrintStream을 상속받고 몇 가지 메쏘드를 오버라이드하면 되지 않을까요? ByteArrayOutputStream을 사용하면 MockObject를 안만들고도 할 수 있겠죠.

  1. 혹은 별도의 인자를 사용하지 않고,

System.setOut 을 이용 리디렉션할 수도 있는데 이 경우 tearDown 등에서 원상 복구해주어야 합니다. LoD를 지키려 한다면 이 방법은 피하는 게 좋습니다. (나중에 구구단의 출력이 화일로 바뀌어야 하는 경우를 생각해 보세요 -- 1번 경우는 코드의 재사용성이 높아집니다) 어찌되건 시스템의 가장자리(사용자 UI, 네트워크, 화일 I/O 등)는 가능하면 얇게 만드는 것이 테스트가능성이나, 코드 디자인 등의 측면에서 바람직합니다. --JuNe


  • Python에서 동적 리스트는 어떻게 해야 할지... 위에 마방진 소스중

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


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

>>> 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적으로 추상화를 할 수 있겠습니다. 저라면 아래 코드 정도의 추상화가 나오도록 코딩을 할 것입니다.

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)이 있고 자신이 있어서 큰 걸음을 한 것인가요? 저라면 위에 getWrapAroundLocation , isPlaceableOn 등도 테스트할 것입니다.

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

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


  • 창준 선배님의 말에 따라 함 고쳐보았습니다. 우선 의미가 불분명한 소스들을

ExtractMethod 를 했습니다. (그러다 보니 벽이 필요 없다는 알고리즘상 문제도 발견했다는...-,-;;;) 그후에 ExtractClass를 했습니다. 물론 그것을 위해 테스트를 먼저 작성 했구요. 그리고 이번에는 전보다 테스트 보폭을 줄이려고 노력했습니다. 다 고치고 전과 후를 비교해보면서 여러가지가 느껴집니다. 특히 전에 짤때는 무심코 넘어갔는데 후에 고칠때는 코드에 냄새가 많이 나네요...^^;;; --재동

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

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)을 알려줘서 바로 바로 고쳤습니다. 다음 요구사항에서는 작은 보폭으로 코딩을 하겠습니다...^^;;; --재동

신재동