2023.02.09 - [분류 전체보기] - 결합과 추상화 - 1 이전 글에 이어서 쓰기
파일 입출력과 관련한 테스트를 어떻게 작성해야 할까? 시스템에서 트릭이 적용된 부분을 분리해서 격리하고, 실제 파일 시스템 없이도 테스트할 수 있게 해야 한다. 외부 상태에 대해 아무 의존성이 없는 코드의 '핵'을 만들고, 외부 세계를 표현하는 입력에 대해 이 핵이 어떻게 반응하는지 살펴보자.
먼저 코드에서 로직과 상태가 있는 부분을 분리하자.
이제 최상위 함수는 거의 아무 로직도 들어있지 않고, 입력을 수집하고 로직(함수형 핵)을 호출한 다음 출력을 적용하는 명령형 코드의 나열로 바뀐다.
def sync(source, dest):
# 명령혈 셀 1단계: 입력 수집
source_hashes = read_paths_and_hashes(source)
dest_hashes = read_paths_and_hashes(dest)
# 명령혈 셀 2단계: 함수형 핵 호출
actions = determine_actions(source_hashes, dest_hashes, source, dest)
# 3단계: 출력 적용
for action, *paths in actions:
if action == "COPY":
shutil.copyfile(*paths)
if action == "MOVE":
shutil.move(*paths)
if action == "DELETE":
os.remove(paths[0])
파일 경로와 파일 해시로 이루어진 dict를 만드는 코드는 쉽게 작성할 수 있다.
def read_paths_and_hashes(root):
hashes = {}
for folder, _, files in os.walk(root):
for fn in files:
hashes[hash_file(Path(folder) / fn)] = fn
return hashes
determine_actions() 함수는 비즈니스 로직의 핵심이다. 이 함수는 간단한 데이터 구조를 입력으로 받고 간단한 데이터 구조를 출력으로 돌려준다.
def determine_actions(source_hashes, dest_hashes, source_folder, dest_folder):
for sha, filename in source_hashes.items():
if sha not in dest_hashes:
sourcepath = Path(source_folder) / filename
destpath = Path(dest_folder) / filename
yield "COPY", sourcepath, destpath
elif dest_hashes[sha] != filename:
olddestpath = Path(dest_folder) / dest_hashes[sha]
newdestpath = Path(dest_folder) / filename
yield "MOVE", olddestpath, newdestpath
for sha, filename in dest_hashes.items():
if sha not in source_hashes:
yield "DELETE", dest_folder / filename
이제 테스는 determine_actions() 함수에 직접 작용한다.
def test_when_a_file_exists_in_the_source_but_not_the_destination():
source_hashes = {"hash1": "fn1"}
dest_hashes = {}
actions = determine_actions(source_hashes, dest_hashes, Path("/src"), Path("/dst"))
assert list(actions) == [("COPY", Path("/src/fn1"), Path("/dst/fn1"))]
def test_when_a_file_has_been_renamed_in_the_source():
source_hashes = {"hash1": "fn1"}
dest_hashes = {"hash1": "fn2"}
actions = determine_actions(source_hashes, dest_hashes, Path("/src"), Path("/dst"))
assert list(actions) == [("MOVE", Path("/dst/fn2"), Path("/dst/fn1"))]
프로그램의 로직(변경을 식별하는 코드)과 저수준 I/O 세부 사항 사이의 얽힘을 풀었기 때문에 쉽게 코드의 핵 부분을 테스트 할 수 있다.
이런 접근 방법을 사용하면 테스트 코드가 주 진입 함수인 sync()를 테스트하지 않고 determine_actions()라는 더 저수준 함수를 테스트학 된다.
'Python' 카테고리의 다른 글
스택프레임, 빅오 (0) | 2023.03.01 |
---|---|
결합과 추상화 - 3 (0) | 2023.02.21 |
Pytest - 3. Monkeypatching functions (0) | 2023.02.03 |
Pytest - 2. Fixture 알아보기 (0) | 2023.02.01 |
Pytest 사용하기 - 1. 간단한 예제 (0) | 2023.01.31 |