소프트웨어 디자인이 단위 테스트와 어떤 관련이 있는지 알아가는 챕터
특정 조건을 보장하는지 검증하는 코드
단위 테스트는 단지 애플리케이션의 "핵심"기능을 검증하는 보조 수단이다 🙅🏻
단위 테스트는 소프트웨어의 핵심으로써 비즈니스 로직과 동일한 수준으로 다뤄져야 한다 🙆🏻♂️
특성 | 내용 | 예시 |
---|---|---|
격리 | 외부 에이전트와 독립적이어야 한다. | 데이터베이스 연결 및 HTTP 요청도 하지 않는다. |
성능 | 신속하게 여러 번 실행될 수 있어야 한다. | |
반복 가능성 | 멱등성을 보장해야 한다. | |
자체 검증(self-validating | 단위 테스트 실행으로 검증 결과가 결정된다. | 테스트 성공 : . , 실패: F, 에러: E |
반복가능성(멱등성)이 깨진 예
python manage.py test --settings=core.settings.test cash.tests.unit
.........................................................................../
..
----------------------------------------------------------------------
Ran 77 tests in 3.882s
OK
python manage.py test --settings=core.settings.test cash.tests.unit.test_cash_receipt모듈.test_ManagerTipCashReceipt
EEEEEEEEEE
FAILED (errors=10)
각각의 테스트가 독립적이지 않아 멱등성이 깨졌다.
그래서 반복 가능하지 않다.
클래스를 테스트 하려면 단위 테스트의 집합인 테스트 스위트(test suite)를 사용한다.
한 번에 여러 컴포넌트를 테스트한다.
HTTP 요청을 하거나 데이터베이스에 연결하는 등의 작업을 수행하는 것이 가능하고 때로는 그렇게 하는 것이 바람직하다.
통합 테스트의 취지는 운영 환경과 유사한 환경에서 기능 검증이지만 여전히 피하고 싶은 의존성이 있을 때도 있다.
예를 들어, 일부 외부 의존성이 인터넷으로 연결된 경우이다. 이런 경우 인터넷 연결 부분을 생략해야 한다.
통합 테스트를 위한 별도의 환경(설정/셋팅 파일)이 존재할 것이고, 도커 컨테이너로 데이터베이스를 mock하도록 설정할 수도 있다.
(다른 의존성에 대해서도 비슷한 방식으로 도커 서비스를 활용한 mock 환경을 구축하는 것이 좋다.)
유스케이스 기반으로 시스템 유효성을 검사하는 자동화된 테스트이다.
개발자는 전체 테스트 스위트를 만들고 코드에 수정이 생길 때마다 반복적으로 빠르게 단위 테스트를 실행하고 리팩터링할 수 있어야 한다.
PR이 생기면 CI 서비스가 실행되어 해당 브랜치에 병합되기 전에 빌드가 실행된다.
실용성이 이상보다 우선이다.
아무도 내가 개발한 시스템을 나보다 잘 알 수는 없다.
때문에 어떤 이유에서든 단위 테스트인데도 도커 컨테이너를 띄워서 데이터베이스 기능을 테스트해야 한다면 그렇게 해야 한다.
에릭 레이먼드(Bric Steven Raymond)가 1997년에 리눅스 회의에서 처음 공개하고 1999년 출간한 책의 이름으로 오픈 소스 철학을 대변한다.
성당 모델은 소수의 개발자만 개발에 참여하고 출시 때 소스 코드를 공개하는 것이다.
시장 모델은 소스 코드가 일반에 공개된 상태로 개발한다.
이 책은 보는 눈만 많다면 어떤 버그라도 쉽게 잡을 수있다는 리누스 법칙에 따라 시장 모델의 도입이 유효할 수 있다고 주장한다.
버그가 많이 포함되어 있더라도 사용자의 피드백을 빨리 받을 수 있도록 "일찍, 그리고 자주 배포하라"와 같은 내용이 책에 언급되어 있다.
단위 테스트가 바로 프로그램이 명세에 따라 정확하게 동작한다는 공식적인 증거가 될 수 있다.
따라서 단위 테스트(혹은 자동화된 테스트)는 우리의 코드가 기대한 것처럼 동작한다는 확신을 줄 수 있는 안정망이 될 수 있다.
테스트의 용이성(Testability) : 소프트웨어를 얼마나 쉽게 테스트 할 수있는지를 결정하는 품질 속성으로써, 클린 코드의 핵심 가치이다.
단위 테스트는 코드를 보완하기 위한 것이 아니라 실제 코드의 작성 방식에 직접적인 영향을 미치는 것이다.
# 특정 작업에서 얻은 지표를 외부 시스템에 보내는 코드
class MetricsClient:
"""타사 지표 전송 클라이언트"""
def send(self, metric_name: str, metric_value: str) -> None:
if not isinstance(metric_name, str):
raise TypeError("metric_name으로 문자열 타입을 사용해야 함")
if not isinstance(metric_value, str):
raise TypeError("metric_value로 문자열 타입을 사용해야 함")
class Process:
def __init__(self):
self.client = MetricsClient()
def process_iterations(self, n_iterations):
for i in range(n_iterations):
result = self.run_process()
self.client.send(f"iterations.{i}", result)
타사에서 제공하는 라이브러리는 우리의 제어권 밖에 있다.
따라서 외부 라이브러리 호출 전에 정확한 타입을 제공하는지 검사해야 한다.
추상화 단계를 하나 더 만들어 개선을 한 코드는 다음과 같다.
Process
비즈니스 모듈은 추상화 모듈인 WrappedClinet
를 의존하고, WrappedClinet
는 MetricClient
를 의존한다.
class WrappedClient:
def __init__(self):
self.client = MetricsClient()
def send(self, metric_name: str, metric_value: str) -> None:
return self.client.send(str(metric_name), str(metric_value))
class Process:
def __init__(self):
self.client = WrappedClient()
# 나머지 코드는 동일
import unittest
from unittest.mock import Mock
class TestWrappedClient(unittest.TestCase):
def test_send_converts_types(self):
wrapped_client = WrappedClient()
wrapped_client.client = Mock()
wrapped_client.send("value", 1)
wrapped_client.client.send.assert_called_with("value", "1")
Mock은 unittest.mock 모듈에서 사용할 수있는 타입으로 어떤 종류의 타입에도 사용할 수있는 편리한 객체이다.
Mock 객체로 시뮬레이션을 한다. send("value", 1)
은 send(str("value"), str(1))
을 호출함으로써 MetricsClient를 mock하는 send 메서드는 두 개의 문자열을 받는 함수라고 정의하고 몽키 패치한다.(몽키 패치 : 런타임 중에 기능을 변경하는 것)
따라서, assert_called_with("value", "1")
은 올바른 파라미터를 사용한 호출이므로 어설트에 성공하게 된다.
물론 이 코드에는 한계가 있다.(클린 코드가 아니다.)
의존성 주입으로 실제 클라이언트가 파라미터 형태로 제공하는 게 훨씬 좋다.
중요한건 단위 테스트를 만드는 과정에서 보다 나은 구현에 대해 생각해 볼 수있게 된다는 것이다.