의존 관계 캡슐화

매일 공부(ML)·2022년 10월 11일
0

파이썬 코딩의 기술

목록 보기
17/27

의존 관계를 캡슐화해서 모킹과 테스트를 쉽게 만들가

  • 래퍼 객체를 사용해서 데이터베이스 인터페이스를 캡슐화하는 것이다.

  • 더 나은 추상화를 사용하면 목이나 테스트를 더 쉽게 만들 수 있다.

    	- 때로는 더 나은 추상화를 사용하도록 코드를 리팩터링할 만한 가치가 있다.
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)

의존 관계 주입의 연결점 역할읗 하는 도우미 함수를 만들어서 ZooDatabase를 프로그램에 주입

  • global 문을 사용하여 모듈 영역에 ZooDatabase를 캐시해주는 도우미 함수를 정의
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
 

Summary

  • 단위 테스트를 작성할 때 목을 만들기 위해 반복적인 준비 코드를 많이 사용해야 한다면, 테스트 대상이 의존하는 다른 기능을 더 쉽게 모킹할 수 있는 클래스로 캡슐화하는 것이 좋다.

  • unittest.mock 내장 모듈의 Mock 클래스는 클래스를 시뮬레이션 할 수 있는 새로운 목을 반환한다. 이 목은 목 메서드처럼 작동할 수 있고 클래스 내 각각의 애트리뷰트에 접근할 수도 있다.

  • 단대단 테스트를 위해서는 테스트에 사용할 목 의존 관계를 주입하는데 명시적인 연결점으로 쓰일 수 있는 도움이 함수를 더 많이 포함하도록 코드를 리팩터링하는 것이 좋다.

profile
성장을 도울 아카이빙 블로그

0개의 댓글