2022.02.12

Jin·2022년 2월 12일
0

xUnit 예시 읽기

실습 소스코드: https://github.com/gringrape/daily-coding-dojo/tree/main/20220212/xUnit

책 읽기

테스트 주도 개발의 2부에 해당 하는 xUnit 예시를 책을 보며 구현해보겠습니다. 책을 읽기 전에 어떻게 하면 책에 천천히 익숙해질 수 있을지를 생각하다, 일정시간동안 책을 훑어보며 먼저 어떤 내용이 있는지 천천히 파악해보기로 했습니다. 2 부는 총 7 개의 장으로 이루어져 있습니다. 25분 동안 전체를 훑어볼까도 생각했지만, 처음은 역시 가벼워야 한다는 생각으로, 18 ~ 20장 까지의 내용을 장당 5분씩 총 15분 동안 읽어보는 것으로 결정했습니다. 읽은 후에는 각 장의 내용을 한문장 이상으로 쓸 수 있는 상태가 되는 것을 완료조건으로 설정했습니다.

18 ~ 20장 까지의 내용을 장당 5분씩 총 15분 동안 읽어보기

첫번째 15분이 지났고, 결과는 실패였습니다. 완료조건을 각 장의 내용을 한 문장 이상으로 말할 수 있다로 잡았는데, 내용이 무엇인지 요약하기위해 모든 내용을 파악하기에는 너무 짧은 시간이라고 느껴졌습니다. 그래서, 당초에 목적을 잊고, 키워드를 수집하는 활동만을 수행했습니다. 키워드만을 찾기에는 또 5분이 많은 시간이었습니다. 욕심을 부리지 않고, 더 짧은 시간(4분)에 더 짧은 분량(1장)으로 키워드 세개를 수집한다 정도의 더 작은 목표를 설정하면 좋았겠다는 생각이 듭니다.

키워드 고르기

다시 18 장으로 돌아와서, 4 분 동안 읽고 키워드 세가지를 골라보겠습니다.

18 장을 4 분 동안 읽고 키워드 세가지를 골라보기

4 분이 지났습니다. 짧은 시간동안 하나의 장에 집중하니, 이전 보다 내용에 더 시간을 알차게 사용한 느낌이 듭니다. 이번에는 큰 목표를 먼저 확인했습니다. 우리가 2부에서 할 일은 테스트 도구를 만드는 일입니다. 18 장의 목표는 이 도구에서 테스트 메서드를 호출 하는 기능을 만드는 것입니다. 제가 고른 키워드는 wasRun, 동적 메서드 호출, 구체적 사례로 부터 일반화 입니다. 키워드라기 보다는 구문이네요. 이 키워드를 조합해서, 처음에는 단순하게 true, false(wasRun) 로 테스트 메서드가 호출되었는지를 검사하고, 점차 발전시켜 나간다는 흐름을 그리게 되었습니다.(과연 맞을까요?) 동적으로 호출한다는 것이 무엇을 말하는것인지 몰라서 다음 독서에서는 조금 더 주의를 기울일 것 같습니다. 4분 동안의 기억을 더듬어보니, 저자가 처음에는 큰 단계를 시도했다가 결과가 좋지않아서 작은 단계로 전환했다는 내용도 생각이 납니다.

이제는 책의 내용을 조금 더 잘 받아들일 수 있는 상태가 되었다는 생각이 듭니다. 시간을 15 분 정도로 확 늘리고 싶지만, 첫번째 시도를 교훈 삼아서, 조금만 증가시키는 것이 좋을것 같습니다. 아직 코딩을 할 만큼 내용이 파악되지는 않아서, 행동목표는 더 쉬운 것을 설정하는게 좋을 것 같습니다. 앞의 시도에서 찾은 주요 구문을 더 구체화 시켜서 목차를 써보는 것은 어떨까요? 잘 안되겠지만 경험이라 생각하고 시도해봅시다. 그 전에, 휴식을 조금 취하는 것을 잊지 맙시다.

목차를 작성해보기

18 장을 7분 동안 읽고, 3분간 주요 구문을 담은 목차로 요약해보기

7분 + 3분, 총 10분이 지났고, 다음과 같이 간단한 목차를 만들 수 있었습니다.

목표 - `테스트 케이스`를 만들고, `테스트 메서드` 를 실행할 수 있어야 한다. 

- 테스트 메서드 실행여부 확인
	-> wasRun 플래그를 통해 테스트가 실행되었는지 여부 검증
    -> test.run() 을 사용하도록 리팩토링
    -> 테스트 메서드 이름을 하드코딩 대신 동적으로 받도록 리팩토링 

사실 글을 다시 읽는 7분동안 굉장히 당혹스러웠는데요, 테스트 케이스테스트 메서드 라는 단어가 반복적으로 등장하는데, 이 단어들이 구체적으로 코드의 어떤 부분에 대응 되는것인지, 매우 헷갈렸습니다. 사실 생각해보면 헷갈릴 이유가 많이 없는 단어이긴 합니다. 우리의 목표가 테스트 도구를 만드는 것이다 보니, 테스트를 작성할때, 테스트 도구를 테스트하는 테스트 같은 식의 문장이 되어버리는 데요, 이렇게 중복된 단어가 혼란을 불러일으키는 것 같습니다. 가까스로 코드를 보면서 엉성한 목차를 작성해 보았습니다.

큰 흐름을 어느 정도 보았으니, 이번에는 엄밀함을 조금 더 채워넣고 싶습니다. 중복된 단어때문에 파악이 힘들었던, 테스트 케이스테스트 메서드 라는 두 단어가 구체적으로 어떤 코드에 대응하는지를 보고 싶습니다. 그러고나면 용기 있게 코드 작성을 시작해 볼 수 있을 것 같습니다. 시간은 12 분이 적절할 것 같습니다. 시간에 쫓기기가 싫고, 중복구조가 만만해 보이지 않을 뿐더러, 이와 유사한 기능을 가진 코드를 작성해본적이 없기 때문에 긴 시간을 잡았습니다.

주요단어의 의미를 세부적으로 읽어보기

12 분 동안 책에서 말하는 테스트 케이스테스트 메서드 가 무엇인지 확인하기

12 분이 지났습니다. 안타깝게도 더 알쏭달쏭해진 느낌이 듭니다. 헷갈리는 원인에는 조금 더 접근했습니다. 우리는 지금 테스트 도구를 테스트하려 하는데, 사용할 도구가 없는 것이지요. 만들면서 사용해야 하는 상황입니다. 책에서는 이런 문제를 부트스트랩 문제라고 말하고 있는것 같네요.

처음에는 WasRun 이라는 테스트 케이스 가 필요합니다. 이 테스트 케이스 안에서 테스트 메서드 가 실행되었는지 여부를 검사하죠. 이 WasRun 을 호출하는 테스트 케이스를 만들면 되는 것입니다. 어째 쓰면서도 헷갈리네요. 안되겠습니다. 이럴 때는 텍스트에 더 집중하기 보다 코드로 넘어가는 것이 좋겠군요. WasRun 테스트 케이스를 만들어 봅시다. 코드가 짧긴 하지만 머리가 혼란스러운 상태이므로, 시간은 15분으로 잡도록 합시다.

15 분 동안 WasRun 구현하기

5 분 정도 코드를 작성하다 이상함을 느꼈습니다. 지금까지는 목표가 뚜렷하지 않아서 읽기의 목표를 스스로 설정해야 했기 때문에 시간의 제한을 두는 것이 좋았지만, 앞으로는 TDD 사이클에서 목표와 피드백을 얻을 것이기 때문에 사이클을 따라가도록 합시다.

TDD 사이클을 따라서 WasRun 구현하기

TDD 사이클에 따라 WasRun 구현하기

WasRun 이라는 테스트 케이스는 wasRun 이라는 플래그를 가지고 있고, 테스트 메서드가 실행되면 플래그 값이 false 에서 true 로 바뀌게 됩니다. 이 스펙 내용을 테스트로 작성해봅시다. 아직은 도구가 없기 때문에, print 함수를 통해 출력합니다.

main.py:

test = WasRun("testMethod")
print(test.wasRun)
test.testMethod()
print(test.wasRun)

실행시켜 줍니다.

python3 main.py

결과:

Traceback (most recent call last):
File "/xUnit/main.py", line 1, in
test = WasRun("testMethod")
NameError: name 'WasRun' is not defined

WasRun 을 정의해 줍시다.

main.py:

class WasRun:
    pass


test = WasRun("testMethod")
print(test.wasRun)
test.testMethod()
print(test.wasRun)

Traceback (most recent call last):
File "/xUnit/main.py", line 5, in
test = WasRun("testMethod")
TypeError: WasRun() takes no arguments

인자를 추가해 줍시다. 지금은 이렇게 피드백을 받아서 컴파일이 되도록 한단계씩 만들어 갑시다.

class WasRun:
    def __init__(self, name):
        pass
# ...

결과:

Traceback (most recent call last):
File "/xUnit/main.py", line 7, in
print(test.wasRun)
AttributeError: 'WasRun' object has no attribute 'wasRun'

testMethod 와 wasRun 속성을 만듭니다.

class WasRun:
    def __init__(self, name):
        self.wasRun = None

    def testMethod(self):
        pass


test = WasRun("testMethod")
print(test.wasRun) # false
test.testMethod()
print(test.wasRun) # true

결과:

None
None

정상적으로 컴파일 됩니다. 가장 빠르게 구현해봅시다.

class WasRun:
    def __init__(self, name):
        self.wasRun = False

    def testMethod(self):
        self.wasRun = True


test = WasRun("testMethod")
print(test.wasRun)
test.testMethod()
print(test.wasRun)

테스트 메서드를 직접 호출하는 대신에 run 이라는 인터페이스를 사용해봅시다.

test = WasRun("testMethod")
print(test.wasRun)
test.run()
print(test.wasRun)

구현을 바꾸면 정상실행됩니다.

class WasRun:
    def __init__(self, name):
        self.wasRun = False

    def testMethod(self):
        self.wasRun = True

    def run(self):
        self.testMethod()

생성자에서 인자로 받은 메서드 이름을 이용하여, 테스트 메서드를 동적으로 실행시켜 주도록 리팩토링 합니다. ("testMethod" 라는 구체적 사례로 부터 일반화해봅니다.)

class WasRun:
    def __init__(self, name):
        self.wasRun = False
        self.name = name

    def testMethod(self):
        self.wasRun = True

    def run(self):
        method = getattr(self, self.name)
        method()

Watch 스크립트

다음 순서로 넘어가기 전에, 파일의 변경이 확인되면 테스트를 실행해주는 간단한 스크립트를 작성해줍시다. 저는 간편하게 작성하기 위해서 google/zx 라이브러리를 사용하였습니다.

terminal:

npm i -g zx

watch.mjs:

#!/usr/bin/env zx

import fs from 'fs';

console.log('Wathching...')

fs.watchFile('./main.py', () => {
  console.log('File changed!')
  $`python3 main.py`
});

terminal:

zx ./watch.mjs

테스트 케이스 일반화 및 실행

이제 책에서는 WasRun 이 하는 일 두가지 (테스트 케이스 실행, 실행 여부 저장) 중 실행 부분을 상위 클래스로 옮깁니다.

class TestCase:
    def __init__(self, name):
        self.name = name

    def run(self):
        method = getattr(self, self.name)
        method()


class WasRun(TestCase):
    def __init__(self, name):
        self.wasRun = False
        TestCase.__init__(self, name)

    def testMethod(self):
        self.wasRun = True


test = WasRun("testMethod")
print(test.wasRun)
test.run()
print(test.wasRun)

마지막으로, 테스트 케이스에 대한 테스트 부분도 테스트 케이스를 상속받아서 구현해 줍니다.

class TestCase:
    def __init__(self, name):
        self.name = name

    def run(self):
        method = getattr(self, self.name)
        method()


class WasRun(TestCase):
    def __init__(self, name):
        self.wasRun = False
        TestCase.__init__(self, name)

    def testMethod(self):
        self.wasRun = True


class TestCaseTest(TestCase):
    def testRunning(self):
        test = WasRun("testMethod")
        assert not test.wasRun
        test.run()
        assert test.wasRun


TestCaseTest("testRunning").run()

회고

think big and start small 이라는 말이 떠오릅니다. 오늘 책을 읽기 시작했던 시점으로 돌아가보면, 내용에 대한 사전지식이 없는 백지 상태였습니다. 이는 무지로 인한 불확실성이 높은 상태라고 볼 수 있는데, 이럴 때는 작게 행동하고 빠르게 피드백 받아서 무지 -> 앎 의 상태로 조금씩 전진해 나가는 것이 필요합니다. 앞으로는 무언가를 배울때도 자신이 어떤 상태에 있는지 확인하고 작게 행동할지 크게 행동할지를 결정해야 겠다는 생각을 합니다.

책읽기에서 목표를 설정하는 부분도 어려웠다는 생각이 듭니다. 책을 읽는 목적이 이해라는 상태에 도달하는 것인데, 이해에 도달했는지 셀프 피드백을 받는 방법을 잘 모르겠습니다. 다만, 중간 중간 결과물을 통해서 읽은 내용을 표현하면서, 어떤 것을 모르고 있는지 확인할 수 있다는 점에서는 의미가 있었습니다.

구현 부분에서는 사실 힘이 많이 빠진 상태로 진행했습니다. 하나의 주제에 대해 시간이 길어지면, 충분한 휴식을 취하거나 맥락 전환을 한 후에 다시 돌아오는 것이 좋겠다는 생각이 듭니다.

++++ 관전 포인트: 테스트 도구를 만들기 위해 테스트 케이스가 실행되는지 검사하는 테스트 케이스를 테스트 주도 개발로 만든다.... 으악

앞으로 할 일

  • 실패하는 경우에도 꺼지지 않도록 watch.mjs 수정하기
  • 19 장 테이블 차리기 읽기

0개의 댓글

관련 채용 정보