• Customer 가 진행하는 것? Closed Beta Test
• QA 가 진행하는 것? Regression Test
• 개발자가 수행하는 것? Integration Test? Unit test?
Functional Testing, Non-Functional Testing 로 크게 나뉘며, 세부적으로 종류 매우 많음.
현실적으로 코드를 작성하는게 우선이므로, 테스트에 대한 (시간)투자는 적어질 수 밖에 없다.
Test Automated should be
영어 키워드 | 한글 번역 및 설명 |
---|---|
Concise | 간결해야 한다 – 가능한 단순하게, 하지만 너무 단순하지 않게. 읽기 쉽고 깔끔해야 함. |
Self-checking | 자체적으로 검증 가능해야 한다 – 테스트 결과를 스스로 판별해야 하며, 사람이 해석할 필요 없어야 함. |
Repeatable | 반복 실행 가능해야 한다 – 여러 번 반복해도 매번 같은 방식으로 실행되어야 함. |
Robust | 견고해야 한다 – 외부 환경 변화(시간, 네트워크 등)에 영향 받지 않고 항상 같은 결과를 내야 함. |
Sufficient | 충분해야 한다 – 소프트웨어의 모든 요구사항을 충분히 검증해야 함. |
Necessary | 불필요한 부분 없이 꼭 필요한 테스트만 있어야 한다 – 테스트 내 모든 코드가 목적에 부합해야 함. |
Clear | 명확해야 한다 – 코드의 모든 문장이 쉽게 이해될 수 있어야 함. |
Efficient | 효율적이어야 한다 – 실행 시간이 너무 길지 않아야 함. |
Specific | 구체적이어야 한다 – 실패 시 어떤 기능이 깨졌는지 정확히 알려줘야 함. (유닛 테스트는 특히 결함 위치 파악에 용이해야 함.) |
Independent | 독립적이어야 한다 – 테스트 하나하나가 따로 실행돼도 잘 동작해야 하고, 순서에 영향 받지 않아야 함. |
Maintainable | 유지보수 가능해야 한다 – 테스트 코드도 쉽게 수정, 확장할 수 있어야 함. |
Traceable | 추적 가능해야 한다 – 테스트가 어떤 코드와 요구사항을 검증하는지 추적할 수 있어야 함. |
어떤 것들이 특히 더 중요할지?
1. ✅ Self-checking (자체 판단 가능)
명칭 | 의미 |
---|---|
Arrange-Act-Assert (AAA) | 준비(Arrange), 실행(Act), 검증(Assert) |
Given-When-Then (GWT) | 상황 설정(Given), 동작 수행(When), 기대 결과 확인(Then) |
Setup-Exercise-Verify-Teardown | 설정, 실행, 검증, 정리(자원 해제 등) |
셋 다 본질은 같고, 테스트를 구조화하는 방법. 네이밍이나 팀 스타일에 따라 선택
테스트를 작성할 때 고려할 3가지 입력 타입
유형 | 설명 |
---|---|
Good Input | 정상적인 입력값 (ex. [1, 2, 3] ) |
Invalid Input | 스펙에서 벗어난 입력값, 예외 처리가 필요한 값 (ex. null , "Test" ) |
Edge Cases | 경계 조건에 해당하는 값 (ex. 빈 리스트, 매우 큰 리스트 등) |
테스트 실패 시 중요한 점
Randomization & Stress Testing
Unit test ≠ TDD
TDD의 정의
Unit Test
TDD Process
Test에서는 동작의 결과만을 확인할 수 있도록. (구현코드는 무관해야한다)
내부에서 DB를 조회하고, 이메일을 보내고, 로그까지 찍는다면… 너무 복잡
→ 그래서 TDD로 getUserContactInfo(), sendEmail(), writeLog() 등으로 쪼개서
각 부분을 독립적으로 테스트 가능하게됨
테스트 코드의 유지보수 노력
좋은 테스트 코드는 작성은 더 어려움
단기간 내 개발 속도 저하
레거시 어려움
UX 개발에서 TDD가 가능한 이유
📌 예시:
<Button />
컴포넌트가 클릭되면 특정 이벤트가 호출되는지 테스트
// 예: React + Testing Library render(<Button onClick={mockFn} />); fireEvent.click(screen.getByText("확인")); expect(mockFn).toHaveBeenCalled();
Given-When-Then
구조로 테스트 가능📌 예시:
“검색창에 ‘hello’를 입력하고 엔터를 누르면 리스트가 나타난다.”
😅 현실적인 제약도 있음
한계점 | 설명 |
---|---|
시각적 요소 테스트는 제한적 | 색상, 정렬, 레이아웃 등은 자동화 테스트로 검증이 어려움 |
Snapshot 테스트 남용 주의 | JSX 구조 비교만 하고, 의미 없는 변화로 테스트 깨지는 경우 많음 |
느림/복잡함 | 브라우저 환경에서 테스트가 돌아가므로 느릴 수 있고 설정도 번거로움 |
TDD 자체가 어렵다 | 디자이너 요구사항이 자주 바뀌면, 먼저 테스트 짜는 게 현실적으로 어려움 |
✨ UX에서 TDD를 잘 적용하려면?
💬 개인적인 팁
UX 중심 개발자라면,
---> UI개발의 경우 TDD보다는 BDD(Behavior) 중심이 대체적인 경향 (python의경우에는 Behave)
테스트 메서드 이름: 의도를 전달할 수 있는 이름
이름만 봐도 무엇을 테스트하는지 명확히 알 수 있게
testUserLogin_whenPasswordIsWrong_shouldFail()
→ 이런 식으로 조건과 기대 결과를 포함한 이름
중복된 테스트 케이스 제거
하나의 테스트에는 하나의 항목만 테스트
하나의 테스트에 여러 assert를 넣는 건 비추.
하나의 테스트는 하나의 동작/조건만 확인해야 실패 시 원인을 명확히 파악할 수 있다
현실적으로 모든 경우를 테스트하는 건 불가능.
→ 중요하고 영향 큰 시나리오 중심으로 테스트
가능한 한 독립적인 테스트 케이스로 작성
테스트 간 상호 의존성이 생기면 순서에 따라 결과가 달라짐
의존성 주의 영역:
DB, 파일시스템, 외부 API, 네트워크 등
→ Mock 또는 Stub 사용을 통해 분리하는 게 중요
25y List Of Python Testing Frameworks (https://www.softwaretestinghelp.com/python-testing-frameworks/)
2PyTest
Python용 테스트 프레임워크
간단한 함수 단위 테스트부터 복잡한 기능 테스트까지 가능
assert 문만으로도 테스트 가능
#설치
pip install pytest
Python 테스트 코드의 네이밍 규칙 (PEP 8 기반)
항목 | 규칙 |
---|---|
테스트 모듈 이름 | test_ 로 시작해야 함 (test_math.py ) |
테스트 함수 이름 | test_ 로 시작해야 함 (test_addition() ) |
테스트 클래스 이름 | Test 로 시작해야 함 (class TestUserLogin: ) |
테스트 메서드 이름 | test_ 로 시작해야 함 (def test_valid_input(self): ) |
테스트 그룹화 | 관련된 테스트는 클래스나 패키지로 묶기 |
패키지 초기화 | 테스트가 들어 있는 디렉토리에는 반드시 __init__.py 파일이 있어야 함 (패키지로 인식되도록 하기 위해) |
calc.py
)`def add(a, b): return a + b`
test_calc.py
)import pytest
from calc import add
def test_add():
assert add(2, 3) == 5
assert add(-1, 1) == 0
초록색 ✅ 뜨면 성공, 빨간색 ❌ 뜨면 실패
assert
– 기본 검증 문법assert <조건식>, <실패 메시지>
예시
def test_function():
assert 1 + 1 == 2
assert 3 % 2 == 0, "홀수입니다"
포인트
pytest.raises
– 예외 발생 테스트with pytest.raises(ExpectedException):
예외 발생 코드
예시 1: ZeroDivisionError
def test_zero_division():
with pytest.raises(ZeroDivisionError):
1 / 0
예시 2: 예외 메시지까지 검증
def test_recursion():
def f(): f()
with pytest.raises(RuntimeError) as e:
f()
assert "maximum recursion" in str(e.value)
포인트
as e:
로 예외 메시지까지 확인 가능@pytest.mark
– 마커로 테스트 그룹 지정import pytest
@pytest.mark.<마커이름>
def test_something():
...
예시
@pytest.mark.smoke
def test_list_raises():
with pytest.raises(TypeError):
tasks.list_tasks(owner=123)
@pytest.mark.get
def test_get_raises():
with pytest.raises(TypeError):
tasks.get(task_id='123')
실행 방법
pytest -m smoke # smoke만 실행
pytest -m get # get만 실행
pytest -m "smoke and get" # 둘 다 실행
팁
pytest.ini
에 마커를 등록하면 경고 없이 관리 가능# pytest.ini 예시
[pytest]
markers =
smoke: 기본 기능 확인용
get: 데이터 조회 관련 테스트
pytest -v
: 테스트 결과 자세히 보기 pytest -k "키워드"
: 특정 이름 포함된 테스트만 실행 pytest --maxfail=1
: 1개 실패 시 종료 pytest --tb=short
: 에러 로그 간단히 출력 Marker : 테스트에 '태그(tag)'를 붙여서 특별한 동작이나 그룹 관리를 할 수 있도록 도와주는 기능
Pytest에서 기본적으로 제공되는 마커들은 테스트를 스킵하거나, 실패를 허용하거나, 반복 실행 등을 지원
마커 | 설명 | 예시 |
---|---|---|
@pytest.mark.skip | 해당 테스트를 무조건 건너뜀 | @pytest.mark.skip(reason="아직 구현 안 됨") |
@pytest.mark.skipif | 조건이 참일 때만 테스트 건너뜀 | @pytest.mark.skipif(sys.platform == "win32", reason="Windows에서 실행 안 함") |
@pytest.mark.xfail | 실패가 예상되는 테스트로 표시. 실패해도 전체 테스트는 통과 | @pytest.mark.xfail(reason="버그 수정 전") |
@pytest.mark.filterwarnings | 테스트 중 발생하는 경고를 필터링 | @pytest.mark.filterwarnings("ignore::DeprecationWarning") |
@pytest.mark.parametrize | 여러 입력값으로 반복 테스트 | @pytest.mark.parametrize("a,b,result", [(1,2,3),(2,2,4)]) |
@pytest.mark.usefixtures | fixture 자동 적용 (함수 내부에서 사용 안 해도 됨) | @pytest.mark.usefixtures("setup_data") |
@pytest.mark.skip
import pytest
@pytest.mark.skip(reason="미완성 테스트")
def test_temp():
assert 1 == 2
임시로 테스트를 건너뛰고 싶을 때
(아직 미구현, 의존 기능 없음 등) 사용
@pytest.mark.skipif
import pytest
import sys
@pytest.mark.skipif(sys.platform == "win32", reason="Windows에서는 실행하지 않음")
def test_linux_only():
assert True
특정 조건(OS, 환경 변수 등)에 따라 테스트를 건너뛰고 싶을 때 사용
@pytest.mark.xfail
import pytest
@pytest.mark.xfail(reason="현재 버전에서 실패 예상")
def test_known_bug():
assert 1 / 0 # 실패해도 테스트는 전체 통과
알고 있는 버그/미완 기능이 있어서 실패를 허용할 때
(CI를 깨지 않고 남겨두고 싶은 테스트) 사용
@pytest.mark.filterwarnings
import pytest
import warnings
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
def test_ignore_warning():
warnings.warn("이건 deprecated 기능입니다.", DeprecationWarning)
assert True
특정 경고 메시지를 무시하거나 필터링하고 싶을 때 사용
@pytest.mark.parametrize
import pytest
@pytest.mark.parametrize("a,b,result", [
(1, 2, 3),
(5, 5, 10),
(0, 0, 0)
])
def test_add(a, b, result):
assert a + b == result
동일한 테스트를 다양한 입력값으로 반복하고 싶을 때(입력 조합을 테스트할 때 매우 강력함) 사용
@pytest.mark.usefixtures
import pytest
@pytest.fixture
def setup_data():
print("데이터 준비!")
@pytest.mark.usefixtures("setup_data")
def test_something():
assert True
setup fixture를 자동으로 실행시키고 싶을 때
(테스트 함수 내에서 직접 호출 없이 실행되도록) 사용- ex 모든 테스트 전에 공통으로 DB 초기화 등
목적 | 사용하는 마커 |
---|---|
테스트 건너뛰기 | skip , skipif |
실패 허용 (기대된 실패) | xfail |
경고 무시 | filterwarnings |
반복 테스트 | parametrize |
미리 fixture 주입 | usefixtures |
테스트 실행 전/후에 필요한 상태 설정, 자원 준비 및 정리를 자동화하는 기능
→ 기존의setup()
/teardown()
을 더 깔끔하게 대체할 수 있음
fixture는 데이터를 주입하고, marker는 테스트에 의미/제어 조건을 붙이는 것.
import pytest
@pytest.fixture
def sample_data():
return {"name": "sunyong", "age": 30}
def test_user_data(sample_data):
assert sample_data["name"] == "sunyong"
📌 설명: sample_data
는 테스트 함수에 인자로 전달되며,
Pytest가 자동으로 실행하고 리턴값을 주입해준다.
yield
사용
yield
를 기준으로 앞쪽은 setup, 뒤쪽은 teardown 역할
@pytest.fixture()
def initialized_tasks_db():
start_tasks_db('temp', 'tiny') # setup
yield
stop_tasks_db() # teardown
yield는 단순한 구분자인가?
📌 실전 팁: DB 연결, 파일 생성, 환경 설정 등을 yield 앞에서 처리하고
테스트 후 자원 정리는 yield 뒤에서 처리하면 안정적이다.
@pytest.fixture
def tasks_just_a_few():
return (
Task("Write some code", "Brian", True),
Task("Code review Brian's code", "Katie", False),
Task("Fix what Brian did", "Michelle", False)
)
@pytest.fixture(autouse=True)
def initialized_tasks_db():
start_tasks_db("temp", "tiny")
yield
stop_tasks_db()
def test_add_using_fixtures(tasks_just_a_few, initialized_tasks_db):
for task in tasks_just_a_few:
task_id = add(task)
t_from_db = get(task_id)
assert equivalent_task(t_from_db, task)
실행 순서 | 내용 |
---|---|
① initialized_tasks_db 실행 | → DB 연결 (setup) |
② tasks_just_a_few 실행 | → 테스트 데이터 준비 |
③ test_add_using_fixtures() 실행 | → 위 두 fixture 결과를 인자로 받아 테스트 수행 |
④ 테스트 끝나면 yield 뒤 teardown 실행 | → DB 종료 |
📌 설명: 여러 개의 fixture를 인자로 받아 사용할 수 있고,
autouse=True
로 설정하면 명시적 인자 없이 자동 실행됨
직접 호출하지 않아도 됨. Pytest가 알아서 fixture 함수를 한 번 실행하고 결과를 주입해준다.
ex) autouse==true
@pytest.fixture(autouse=True)
def logger():
print("📝 로그 시작!")
def test_something(): # 👈 logger를 인자로 안 써도 자동 실행됨
print("🧪 테스트 실행")
###📝 로그 시작!
###🧪 테스트 실행
ex) autouse==false
@pytest.fixture
def logger():
print("📝 로그 시작!")
def test_something(logger): # 👈 여기서 명시해줘야 logger 실행됨
print("🧪 테스트 실행")
###📝 로그 시작!
###🧪 테스트 실행
@pytest.fixture(scope="module")
def db_conn():
# 테스트 파일 단위로 1번만 실행됨
...
Scope | 실행 범위 |
---|---|
function | 테스트 함수마다 (기본값) |
module | 모듈(파일)당 1회 |
class | 클래스 당 1회 |
session | 전체 테스트 세션 중 1회 |
📌 실전 예시: DB 연결, API 클라이언트 초기화 → session
테스트 유저 세팅, 모듈별 연결 등 → module
tasks_to_try = (
Task("sleep", done=True),
Task("wake", "brian")
)
@pytest.fixture(params=tasks_to_try)
def a_task(request):
return request.param
📌 설명: params
를 지정하면 Pytest는 fixture를 값마다 반복 실행함
→ 다양한 입력 조합을 자동으로 테스트할 수 있음
# 📁 tests/conftest.py
@pytest.fixture(scope='session')
def tasks_just_a_few():
return (
Task("Write some code", "Brian", True),
Task("Code review Brian's code", "Katie", False),
Task("Fix what Brian did", "Michelle", False)
)
📌 설명: 테스트 디렉토리 하위에 conftest.py
파일을 만들면
해당 폴더의 모든 테스트 파일에서 import 없이 자동 사용 가능!
Fixture | 설명 |
---|---|
capsys | stdout , stderr 출력 캡처 |
caplog | 로그 메시지 캡처 |
monkeypatch | 함수/속성/환경변수 임시 변경 |
tmp_path | 임시 디렉토리 경로 제공 (Pathlib) |
request | 현재 테스트 정보 접근 |
pytestconfig | CLI 설정 값 접근 (--option 등) |
def test_print_output(capsys):
print("hello world")
out, err = capsys.readouterr()
assert "hello" in out
@pytest.fixture()
는 코드 중복을 줄이고 테스트 흐름을 깔끔하게 구성할 수 있는 핵심 도구yield
로 setup/teardown 구분, scope
와 params
로 유연한 테스트 전략 설계conftest.py
를 활용해 프로젝트 전체에서 fixture를 공유하면 테스트가 훨씬 간결해진다conftest.py
– 공통 fixture 보관소여러 테스트 파일에서 사용하는 공통 fixture를 저장해놓는 중앙 관리 파일
→ Pytest가 자동으로 인식해서, import 없이도 사용할 수 있음
project/
├── src/
├── tests/
│ ├── conftest.py
│ ├── test_api.py
│ ├── test_user.py
# tests/conftest.py
import pytest
@pytest.fixture(scope="session")
def tasks_just_a_few():
return (
Task("Write some code", "Brian", True),
Task("Code review Brian's code", "Katie", False),
Task("Fix what Brian did", "Michelle", False),
)
# tests/test_user.py
def test_user_data(tasks_just_a_few):
assert tasks_just_a_few[0].owner == "Brian"
monkeypatch
– 테스트 환경 임시 변경 도구테스트 중에 클래스 속성, 함수, 딕셔너리 값, 환경변수, 작업 디렉터리 등을 임시로 수정하고, 테스트 후 자동 복구되는 기능
setattr(obj, name, value)
delattr(obj, name)
setitem(mapping, name, value)
delitem(mapping, name)
setenv(name, value)
delenv(name)
chdir(path)
syspath_prepend(path)
DEFAULT_CONFIG = {"user": "user1", "database": "db1"}
def create_connection_string(config=None):
config = config or DEFAULT_CONFIG
return f"User Id={config['user']}; Location={config['database']};"
@pytest.fixture
def mock_test_user(monkeypatch):
monkeypatch.setitem(DEFAULT_CONFIG, "user", "test_user")
@pytest.fixture
def mock_test_database(monkeypatch):
monkeypatch.setitem(DEFAULT_CONFIG, "database", "test_db")
def test_connection(mock_test_user, mock_test_database):
expected = "User Id=test_user; Location=test_db;"
result = create_connection_string()
assert result == expected
request
fixture@pytest.fixture(params=[1, 2, 3], ids=["one", "two", "three"])
def data(request):
return request.param * 10
def test_data_values(data, request):
print(f"Testing with data: {data}, id: {request.node.name}")
request.param
, request.scope
, request.function
, request.getfixturevalue(name)
등으로 활용capsys
, capfd
)def my_function():
print("stdout message")
import sys
sys.stderr.write("stderr message\n")
def test_output(capsys):
my_function()
captured = capsys.readouterr()
assert "stdout" in captured.out
assert "stderr" in captured.err
tmp_path
, tmp_path_factory
)def test_tmp_path(tmp_path):
file = tmp_path / "file.txt"
file.write_text("hello")
assert file.read_text() == "hello"
def test_tmp_path_factory(tmp_path_factory):
dir = tmp_path_factory.mktemp("data")
file = dir / "file.txt"
file.write_text("abc")
기능 | 설명 |
---|---|
conftest.py | 여러 테스트 간 공통 fixture 저장소 |
monkeypatch | 테스트 중 환경/속성/값 임시 변경 |
request | 현재 테스트 컨텍스트 정보 접근 |
capsys 등 | 입출력 캡처 (stdout/stderr) |
tmp_path | 임시 파일 및 디렉토리 테스트 |
pytestconfig , cache | 설정 정보 접근 및 값 저장 |
Test Double
실제 객체(클래스, 함수 등)의 역할을 대신하는 가짜 객체를 의미.
→ 테스트에서만 임시로 사용되는 객체
이름 | 설명 | 예시 상황 |
---|---|---|
Dummy | 아무 역할x, 껍데기. 인스턴스 수준의 객체 | None 대신 넘겨주는 빈 객체 |
Fake | 실제 구현과 유사하지만 간단한 구현을 가짐 | 실제 DB 대신 메모리 DB 사용 |
Stub | 고정된 값을 반환하는 가짜 함수(특정 모습,상태를 가정한 객체) | 서버에서 항상 "ok" 반환 |
Spy | 테스트동안 호출 여부나 횟수, 인자 등을 기록함 | "이 함수 몇 번 호출됐지?" 추적 |
Mock | Spy + Stub. 호출 기록도 추적하고, 행동을 미리 정의할 수 있음. 특정한 동작이 올바르게 수행되었는지 여부와 같은 behavior를 테스트. | "이 함수가 이 인자로 1번 호출돼야 해"를 검증 |
pytest-mock
은 unittest.mock
을 Pytest에서 더 쉽게 쓰기 위한 thin wrappermocker
fixture를 통해 patch
, Mock
, MagicMock
등 사용 가능def test_foo(mocker):
mocker.patch('os.remove') # os.remove 함수 mocking
mocker.patch.object(os, 'listdir', autospec=True)
mocked_isfile = mocker.patch('os.path.isfile')
patch()
: 대상 함수/속성을 mock 객체로 대체patch.object()
: 객체의 특정 속성만 patchpatch.dict()
, patch.multiple()
등도 지원Mock
: 일반 mock 객체MagicMock
: __str__
, __iter__
, __len__
등 magic method 지원PropertyMock
: 클래스의 property mocking 전용mock_randint = mocker.Mock(return_value=10)
assert mock_randint() == 10
옵션 | 설명 |
---|---|
spec | 실제 객체에 없는 속성 접근 시 에러 발생 |
side_effect | 호출될 때 동작 또는 예외 발생 설정 |
return_value | 호출 결과값 설정 |
wraps | 원래 객체 감싸기 (spy와 유사) |
name | 디버깅용 mock 이름 지정 가능 |
메서드 | 설명 |
---|---|
assert_called() | 최소 1번 호출됨 |
assert_called_once() | 정확히 1번 호출됨 |
assert_called_with(*args) | 지정한 인자로 호출됨 |
assert_called_once_with(*args) | 정확히 1번, 지정 인자로 호출됨 |
assert_any_call(*args) | 인자로 최소 1번 호출됨 |
assert_has_calls([...]) | 호출 목록 일치 검사 |
assert_not_called() | 호출되지 않음 |
mocker.ANY
: 어떤 인자든 허용mocker.sentinel.X
: 고유한 식별용 mock 객체mocker.DEFAULT
: patch나 side_effect에서 기본 동작 사용mocker.call
: 호출 이력을 확인할 때 사용mocker.seal(mock)
: 이후 속성 추가 불가능하게 고정mock = mocker.Mock()
mock("hi")
assert mock.call_args == mocker.call("hi")
class Foo:
def bar(self, x):
return x * 2
foo = Foo()
spy = mocker.spy(foo, 'bar')
assert foo.bar(3) == 6
spy.assert_called_once_with(3)
assert spy.spy_return == 6
def foo(cb):
cb("a", "b")
stub = mocker.stub(name="on_event")
foo(stub)
stub.assert_called_once_with("a", "b")
mocker.patch("module.func") # 기본 patch
mocker.patch.object(obj, "attr") # 객체 속성만 patch
mocker.patch.dict(my_dict, {"key": "value"}) # 딕셔너리 patch
mocker.patch.multiple("module", a=..., b=...) # 여러 속성 한 번에
이름 | 설명 |
---|---|
mocker | 기본: function scope |
class_mocker | 테스트 클래스 전체에서 공유 |
module_mocker | 테스트 모듈 전체에서 공유 |
package_mocker | 패키지 전체 공유 |
session_mocker | 테스트 전체 세션 동안 공유 |
기능 | 설명 |
---|---|
Mock | 가장 기본적인 mock 객체 |
MagicMock | magic method 포함된 mock |
spy | 실제 객체 감시, 호출 추적 가능 |
stub | 콜백 확인 등 간단한 mock |
patch 계열 | 다양한 방법으로 mock 구성 |
sentinel , ANY 등 | assert 시 유용한 도우미 객체 |
테스트에서 의존성을 분리하고 테스트 가능한 구조로 만들기 위한 다양한 기법들
recalculate()
→ 테스트용 another_recalculate()
로 교체무한 루프나 종료 조건이 복잡한 경우 → 조건을 함수로 분리해서 외부(특히 테스트 코드)에서 통제할 수 있게 만든다
예시:
while get_temperature() < 70:
keep_heating()
get_temperature()는 실제 센서 값을 읽을 수도 있고 무한루프될 수 있어 테스트가 힘듬.
def should_continue_loop():
# 실제 구현에서는 센서값을 체크할 수도 있고,
# 테스트에서는 반복 횟수 제한 등으로 대체할 수도 있음
return get_temperature() < 70
def infinite_loop():
while should_continue_loop():
keep_heating()
test code에서 아래와 같이 사용
class TestHeater:
def __init__(self):
self.count = 0
def should_continue_loop(self):
self.count += 1
return self.count < 3 # 3번만 실행하게 강제
def test_infinite_loop():
heater = TestHeater()
loop = InfiniteLoop(should_continue=heater.should_continue_loop)
loop.run()
assert heater.count == 3
조건 캡슐화는 실제 구현 코드를 테스트하기 좋게 바꾸는 리팩토링.
테스트를 위한 의존성 주입(Dependency Injection)의 일환으로 실제 코드의 루프 조건 자체를 함수로 분리하는 방식.
make_request()
오버라이드해서 의도적 예외 발생협력 객체(의존 객체)를 테스트에 맞게 가짜 객체(Fake), 스텁(Stub), 혹은 목(Mock) 등으로 교체하는 기법 (테스트 대상 코드가 어떤 외부 클래스(협력자, Collaborator)에 의존하고 있다면, 테스트 전용 객체를 사용하는 것)
예시:
connection = OverrideConnection()
sut = NetRetriever(connection)
OverrideConnection(Connection):
def open(self):
raise RemoteException()
def test_retrieval_response_for_exception():
with pytest.raise(RetrievalException):
connection = OverrideConnection()
sut = NetRetriever(connection)
sut.retrieve_response_for(None)
class NetRetriever():
def __init__(self, connection):
self.connection = connection
def retrieve_response_for(self, request):
self.connection.open() # asis - 협력자 호출 : 에러가 날 수 있음. tobe - 협력자 대체 호출
# ...
목적 | 설명 |
---|---|
예측 가능한 테스트 | 외부 상태나 동작에 영향 안 받음 |
에러 시나리오 검증 | 예외 발생 등 실제 상황을 시뮬레이션 가능 |
성능 향상 | 느린 외부 리소스(DB, 네트워크 등) 없이 테스트 |
캡슐화 유도 | 결합도 줄이고 유연한 설계 유도 |