래퍼 객체를 사용해서 데이터베이스 인터페이스를 캡슐화하는 것이다.
더 나은 추상화를 사용하면 목이나 테스트를 더 쉽게 만들 수 있다.
- 때로는 더 나은 추상화를 사용하도록 코드를 리팩터링할 만한 가치가 있다.
class ZooDatabase:
...
def get_animals(self, species):
...
def get_food_period(self, species):
...
def feed_animal(self, name, when):
...
#do_rounds함수가 ZooDatabase 객체의 메서드를 호출하게 변경
from datetime import datetime
def do_rounds(database, species, *, utcnow=datetime.utcnow):
now = utcnow()
feeding_timedelta = database.get_food_period(species)
animals = database.get_animals(species)
fed = 0
for name, last_mealtime in animals:
if (now-last_mealtime) >= feeding_timedelta:
database.feed_animal(name, now)
fed +=1
return fed
이제 unittest.mock.path를 사용해서 목을 테스트 대상 코드에 주입할 필요가 없기에 do_rounds에 대한 테스트를 작성하기가 더 쉽다.
path를 사용하는 대신, 이제 ZooDatabase를 표현하는 Mock 인스턴스를 만들어서 do_rounds의 database로 넘길 수 있다.
Mock 클래스는 자신의 애트리뷰트에 대해 이뤄지는 모든 접근에 대해 목 객체를 반환한다.
from unittest.mock import Mock
database = Mock(spec=ZooDatabase)
print(database.feed_animal)
database.feed_animal()
database.feed_animal.assert_any_call()
#<Mock name='mock.feed_animal' id='4384773408'>
ZooDatabase 캡슐화를 사용하도록 Mock 설정 코드를 다시 작성할 수 있다.
from datetime import timedelta
from unittest.mock import call
now_func = Mock(spec=datetime.utcnow)
now_func.return_value = datetime(2019,6,5,15,45)
database = Mock(spec=ZooDatabase)
database.get_food_period.return_value = timedelta(hours=3)
database.get_animals.return_value = [
('점박이', datetime(2019,6,5,11,15)),
('털보', datetime(2019,6,5,12,30)),
('조조', datetime(2019,6,5,12,55)),
]
이제 테스트 대상 함수를 실행하여, 함수가 의존하는 모든 메서드가 예상대로 호출되었는지 검증
result = do_rounds(database, '미어캣', utcnow = now_func)
assert result == 2
database.get_food_period.assert_called_once_with('미어캣')
database.get_animals.assert_called_once_with('미어캣')
database.feed_animal.assert_has_calls(
[
call('점박이', now_func.return_value),
call('털보', now_func.return_value),
],
any_order=True)
DATABASE = None
def get_database():
global DATABASE
if DATABASE is None:
DATABASE = ZooDatabase()
return DATABASE
def main(argv):
database = get_database()
species = argv[1]
count = do_rounds(database, species)
print(f'급양: {count} {species}')
return 0
모킹하는 방식보다 신뢰성은 약간 떨어지지만 더 많은 영역을 테스트할 수 있다.
import contextlib
import io
from unittest.mock import patch
with patch('__main__.DATABASE', spec = ZooDatabase):
now = datetime.utcnow()
DATABASE.get_food_period.return_value = timedelta(hours=3)
DATABASE.get_animals.return_value = [
('점박이', now - timedelta(minutes=4.5)),
('털보', now - timedelta(hours=3.25)),
('조조', now-timedelta(hours=3)),
]
fake_stdout = io.StringIO()
with contextlib.redirect_stdout(fake_stdout):
main(['프로그램 이름', '미어캣'])
found = fake_stdout.getvalue()
expected = '급양: 2 미어캣\n'
assert found == expected
단위 테스트를 작성할 때 목을 만들기 위해 반복적인 준비 코드를 많이 사용해야 한다면, 테스트 대상이 의존하는 다른 기능을 더 쉽게 모킹할 수 있는 클래스로 캡슐화하는 것이 좋다.
unittest.mock 내장 모듈의 Mock 클래스는 클래스를 시뮬레이션 할 수 있는 새로운 목을 반환한다. 이 목은 목 메서드처럼 작동할 수 있고 클래스 내 각각의 애트리뷰트에 접근할 수도 있다.
단대단 테스트를 위해서는 테스트에 사용할 목 의존 관계를 주입하는데 명시적인 연결점으로 쓰일 수 있는 도움이 함수를 더 많이 포함하도록 코드를 리팩터링하는 것이 좋다.