테스트 코드 작성 방법
파이썬 패키지를 개발 중이야. 테스트코드 작성을 도와줘. 예를 들어서 pip install . 하면 mypackage라는 패키지가
설치돼. 프로젝트 소스 폴더 안에는 pyproject.toml, mypackage(*.py 소스 보관)이 있어. 이 상태에서
mypackage.parse라는 함수에 대한 테스트코드를 작성하고 싶어. 여러 테스트셋 인풋 파일을 parse 함수에 넣어서 테스트하려고
해.
네, 정확하게 진행하려면 다음 단계로 접근하는 것이 좋습니다. 테스트 코드를 효율적으로 작성하려면 pytest를 사용하는 것이
일반적입니다. 다음은 step-by-step 설명과 예시입니다.
✅ 가정
- 프로젝트 구조는 다음과 같다고 가정하겠습니다:
your-project/
├── pyproject.toml
├── mypackage/
│ ├── __init__.py
│ └── parse.py # 여기에 parse 함수가 있음
├── tests/
│ ├── __init__.py
│ └── test_parse.py # 테스트 코드
└── test_inputs/
├── input1.txt
├── input2.txt
└── ...
✅ parse 함수 테스트: 기본 흐름
먼저 mypackage/parse.py 안에 다음과 같이 parse 함수가 정의되어 있다고 가정하겠습니다:
# mypackage/parse.py
def parse(text: str) -> dict:
# 예시 파싱 함수
lines = text.strip().splitlines()
return {"lines": lines, "line_count": len(lines)}
✅ 테스트 코드 예시 (tests/test_parse.py)
import os
import pytest
from mypackage.parse import parse
# 테스트 데이터 폴더 경로
TEST_INPUT_DIR = os.path.join(os.path.dirname(__file__), '..', 'test_inputs')
# 테스트 케이스 리스트 만들기
test_files = [
"input1.txt",
"input2.txt",
# 필요하면 더 추가
]
@pytest.mark.parametrize("filename", test_files)
def test_parse_from_file(filename):
path = os.path.join(TEST_INPUT_DIR, filename)
with open(path, "r", encoding="utf-8") as f:
content = f.read()
result = parse(content)
# 여기서 기대 결과를 어떻게 판단할지가 핵심입니다
# 일단 단순하게 구조 확인
assert isinstance(result, dict)
assert "lines" in result
assert "line_count" in result
assert len(result["lines"]) == result["line_count"]
✅ 추가 팁
-
테스트 파일 기대값 정의
기대 결과가 명확해야 하면input1_expected.json같은 파일로 기대값을 따로 두고 비교하면 좋습니다. - 테스트 실행
pytest - pyproject.toml에
pytest설정
tool.pytest.ini_options섹션을 넣어도 좋습니다 (선택 사항).
❗주의할 점
-
parse가 단순 문자열이 아닌 파일을 직접 열도록 되어 있다면,parse_from_file같은 wrapper를 만드는 것이 테스트하기 편합니다. -
테스트에 필요한 파일이 많아질수록, fixture나 test data generator 사용을 고려해 보세요.
필요하시면 fixture 버전이나 기대값을 비교하는 구조도 작성해 드릴 수 있습니다. parse 함수의 구체적 역할이나 출력 형식이
알려지면 더 정밀하게 짤 수 있습니다. 알려주시면 이어서 도와드리겠습니다.
fixture가 뭐야?
좋은 질문입니다. fixture는 pytest에서 테스트 코드 실행 전에 공통적으로 필요한 준비 작업을 해주는 함수 입니다.
예를 들어 테스트에 반복적으로 필요한 파일 읽기, 객체 생성, 환경 설정 등을 한 번만 해두고, 각 테스트 함수에서 재사용할 수 있게
해줍니다.
✅ 예제: 파일 읽기 fixture
테스트 전에 매번 파일을 읽는 코드가 있다면:
def test_parse_input1():
with open("test_inputs/input1.txt") as f:
data = f.read()
result = parse(data)
assert ...
이걸 여러 번 반복하면 귀찮고 중복이 생기죠.
✅ fixture로 정리하면
import pytest
@pytest.fixture
def input1_text():
with open("test_inputs/input1.txt", encoding="utf-8") as f:
return f.read()
def test_parse_input1(input1_text):
result = parse(input1_text)
assert ...
-
@pytest.fixture가 붙은 함수는 테스트 함수에서 인자로 받을 수 있습니다. -
test_parse_input1()함수는input1_text()함수를 자동으로 호출해서 그 값을 받습니다. -
이름은 자유롭게 정할 수 있지만 의미를 명확하게 해야 합니다.
✅ fixture는 어디에 둬야 할까?
공통으로 쓰는 fixture가 많아지면 conftest.py 파일에 모아두는 게 일반적입니다.
your-project/
├── tests/
│ ├── conftest.py 👈 fixture 모음
│ ├── test_parse.py
✅ fixture는 언제 쓰는 게 좋나?
-
테스트 데이터 로딩 (파일, DB 등)
-
테스트용 객체 생성 (예: 가짜 유저, mock API)
-
공통 환경 설정 (예: temp 폴더 만들기, 임시 설정 적용)
-
cleanup이 필요한 작업 (예: 파일 삭제, 상태 복원)
정리하자면:
fixture는 테스트 실행 전 필요한 준비 작업을 미리 해주는 자동화 도구입니다.
반복을 줄이고, 테스트 코드의 가독성과 유지보수성을 크게 높여줍니다.
더 복잡한 예, 예를 들어 여러 파일을 매개변수화해서 fixture에 넣고 싶으시면 알려주세요. 거기까지도 가능합니다.
test_inputs를 tests 폴더 안에 넣는 것과 너의 방식의 장단점을 비교해줘
아주 중요한 포인트를 짚으셨습니다.
테스트 입력 파일(test_inputs)을 tests/ 폴더 안에 넣을지, 혹은 루트에 둘지 는 프로젝트 구조,
유지보수, 배포 방식에 따라 장단점이 분명합니다. 비교해드리겠습니다.
🔧 방식 1: test_inputs/를 루트에 두는 방식 (tests/ 바깥)
✅ 장점
| 항목 | 설명 |
|---|---|
| 프로젝트 논리적 분리 | 테스트 코드(tests/)와 테스트 데이터(test_inputs/)가 분리되어 역할이 명확함 |
| 여러 테스트 스위트에서 공유 가능 | 예: unit tests, integration tests 등 여러 테스트 폴더에서 같은 test_inputs/를 쓸 수 있음 |
| 에디터/IDE에서 탐색이 편함 | tests/만 봐도 테스트 코드만 깔끔하게 보임 |
❌ 단점
| 항목 | 설명 |
|---|---|
| 상대 경로가 복잡해짐 | 테스트 코드에서 ../test_inputs/... 형태로 상대 경로를 써야 함 |
| 이동성 저하 | tests/만 따로 옮기거나 복사하면 test_inputs/ 누락 위험 있음 |
| 배포 시 누락 주의 필요 | PyPI에 배포할 때 test_inputs는 포함시키지 않아야 함 (별도 관리 필요) |
🔧 방식 2: test_inputs/를 tests/ 폴더 안에 넣는 방식 (tests/test_inputs/)
✅ 장점
| 항목 | 설명 |
|---|---|
| 폴더 하나로 테스트 완결 | tests/ 하나만 복사해도 테스트 가능. 독립성과 이동성 높음 |
| 상대 경로 간단함 | "test_inputs/input1.txt" 처럼 간단하게 접근 가능 |
| 테스트 전용으로 분명히 구분됨 | 배포/운영 코드에 테스트 자원이 섞이지 않음. 빌드 시스템에도 명확하게 테스트 전용으로 인식됨 |
❌ 단점
| 항목 | 설명 |
|---|---|
| 공유 어려움 | 다른 테스트 폴더에서 재사용하려면 import 경로나 경로 계산이 복잡해짐 |
| 테스트와 데이터가 섞여 복잡 | 테스트 코드 파일과 데이터 파일이 같이 있어서 구조가 다소 혼잡할 수 있음 (정리는 가능) |
📌 어떤 걸 선택해야 할까?
| 상황 | 추천 방식 |
|---|---|
| 작은 프로젝트 / 단일 테스트 종류 | tests/test_inputs/ (내부에 포함) 추천 |
| 복잡한 프로젝트 / 여러 테스트 스위트 / CI 분리 필요 | test_inputs/ 외부 분리 추천 |
| 테스트 데이터가 많고 재사용이 필요한 경우 | 외부 분리 (루트에 test_inputs/) |
💡 결론
단순하고 깔끔한 테스트 환경이 목적이라면 →
tests/test_inputs/
여러 스위트나 고급 구성이 필요하다면 → 루트에test_inputs/
의심 많은 개발자라면 이렇게 판단하면 됩니다:
-
테스트만 보관할 목적이라면 안에,
-
공유/분리가 필요하다면 밖에.
원하시면 pyproject.toml에서 테스트 리소스 경로를 처리하는 방식도 같이 알려드릴 수 있습니다.
tests 폴더 내부에 테스트 데이터들을 넣는다면, test_inputs 대신에 쓸만한 폴더명 추천해줘
좋은 질문입니다. tests/ 내부에 테스트 데이터를 넣는다면, test_inputs/보다는 의미를 더 명확하게 하거나 범용성 있게
바꾸는 것이 좋습니다. 목적에 따라 다음과 같은 폴더명을 추천드립니다:
📁 추천 폴더명 목록
| 폴더명 | 쓰기 좋은 상황 | 설명 |
|---|---|---|
| fixtures/ | 일반적인 테스트 입력 파일을 포함할 때 | pytest 용어와도 일치. 범용적이고 표준화된 명칭 |
| data/ | 테스트용 데이터가 다양하고 정형/비정형이 섞일 때 | CSV, JSON, 텍스트 등 다양한 포맷에 적합 |
| resources/ | 테스트에 필요한 모든 리소스를 담을 때 | 테스트 입력 외에도 이미지, 설정 파일 등이 있을 경우 |
| inputs/ | 입력 중심 테스트일 때 | 테스트 대상 함수에 넣을 raw input 중심일 경우 |
| samples/ | 예시 기반 테스트일 때 | 실제 입력 예시나 샘플 로그 등을 테스트할 경우 적합 |
| assets/ | 경량 정적 리소스 느낌일 때 | 텍스트, 이미지, 설정 등 테스트용 "정적 자산"에 적합 |
| test_data/ | 보수적이고 명확한 이름을 원할 때 | 직관적이고 오해의 여지가 없음. 단조롭지만 명확함 |
🧠 의문을 가져야 할 포인트
-
이 폴더가 무엇을 담을 것인가? → 텍스트? JSON? 바이너리?
-
다른 테스트 유형에서도 공유할 것인가? → integration, functional test 등?
-
CI 환경에서 어떻게 다룰 것인가? → 불필요한 리소스 포함되지 않도록 관리 필요
✅ 내 추천 (요약)
| 목적 | 추천 이름 |
|---|---|
| 범용성 & 깔끔함 | fixtures/ ✅ |
| 직관적이고 보수적 | test_data/ ✅ |
| 입력 위주 테스트 | inputs/ 또는 samples/ |
개인적으로는 tests/fixtures/ 또는 tests/test_data/를 가장 추천합니다.
의심스러운 선택을 피하려면 보수적으로 test_data/ 쓰는 것도 괜찮습니다.
테스트 출력을 파일로 할거야. output 폴더 안에 하고 싶은데 테스트 시작 전에 이미 output 폴더가 존재한다면 지우고 다시 만들게 하고 싶어.
네, 그 요구사항은 완전히 타당하고, 테스트 신뢰성을 위해 tests/output/ 폴더를 항상 깨끗한 상태로 초기화하는 건 매우
중요합니다. 이걸 처리하는 가장 깔끔한 방법은 pytest의 fixture를 이용하는 것입니다.
✅ 목표
-
tests/output/폴더가 테스트 시작 전 존재한다면 삭제 -
그 후 빈 폴더로 다시 생성
✅ 구현 방법: conftest.py에서 fixture 정의
# tests/conftest.py
import shutil
import os
import pytest
@pytest.fixture(scope="session", autouse=True)
def clean_output_folder():
output_dir = os.path.join(os.path.dirname(__file__), "output")
# 폴더가 존재하면 삭제
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
# 새로 생성
os.makedirs(output_dir, exist_ok=True)
✅ 어떻게 작동하나?
-
scope="session": pytest 실행 전체 세션 동안 딱 한 번 실행됨 -
autouse=True: 어떤 테스트에서도 명시적으로 호출하지 않아도 자동 실행됨
즉, pytest를 실행하면:
-
tests/output/이 있으면 삭제됨 -
새로 만들어짐
-
모든 테스트가 그 깨끗한 상태를 공유함
❗주의
-
output/폴더에 중요한 파일이 들어있지 않아야 합니다. 이 fixture는 강제 삭제 합니다. -
테스트 실행 중간에 또 초기화하고 싶다면 scope를
"function"으로 줄이고, 각 테스트에 fixture를 명시해줘야 합니다.
✅ 테스트에서 사용 예시
def test_parse_and_save_to_file():
output_path = os.path.join(os.path.dirname(__file__), "output", "result1.json")
with open(output_path, "w", encoding="utf-8") as f:
f.write("some result")
assert os.path.exists(output_path)
✋ 의심해봐야 할 것
-
output/을 지운다는 게 실제로 "안전한가?" → Git에 커밋된 다른 테스트 리소스가 이 폴더에 있으면 안 됨 -
동시에 여러 테스트가 파일을 쓰는 구조인가? → 그럼 파일명 충돌, race condition을 고려해야 함
필요하면 테스트마다 고유한 output path 를 자동으로 만들어주는 fixture도 짜드릴 수 있습니다. 원하시면 말씀 주세요.
기능별로 tests 폴더 안에 subdirectory를 만든 상태야. 그 안의 output을 지우게 하고 싶어.
좋습니다. 그런 구조라면 tests/ 안의 모든 서브디렉토리 안에 있는 output/ 폴더를 찾아서 지우고 다시 생성하는
방식이 필요합니다.
✅ 예시 디렉토리 구조
lua
tests/
├── feature_a/
│ ├── test_something.py
│ └── output/
├── feature_b/
│ ├── test_something_else.py
│ └── output/
└── conftest.py
✅ 목표
pytest 시작 전에 다음을 자동으로 수행:
-
tests/**/output/폴더 모두 재귀 탐색 -
존재하면 삭제하고, 빈 폴더로 재생성
✅ conftest.py 예시 (최상위 tests/ 안에 둠)
# tests/conftest.py
import os
import shutil
import pytest
@pytest.fixture(scope="session", autouse=True)
def clean_all_output_dirs():
base_dir = os.path.dirname(__file__)
for root, dirs, files in os.walk(base_dir):
for d in dirs:
if d == "output":
output_path = os.path.join(root, d)
print(f"[pytest setup] Cleaning: {output_path}")
# 삭제 후 다시 생성
shutil.rmtree(output_path, ignore_errors=True)
os.makedirs(output_path, exist_ok=True)
✅ 장점
-
tests/내부 어떤 깊이든output/폴더가 있으면 모두 처리함 -
테스트 시작 전 단 한 번 실행됨 (
session,autouse=True)
❗의심해볼 점
| 질문 | 이유 |
|---|---|
| 정말 모든 output 폴더를 지워도 되는가? | 혹시 로그나 캐시로 남겨야 하는 파일은 아닌가? |
| 테스트 실행 중 output 경로가 충돌하지 않는가? | 멀티스레드/병렬 테스트에서 겹치면 문제가 생길 수 있음 |
| 삭제 실패에 대비할 로직이 있는가? | 파일이 잠겨있거나 권한 문제로 삭제가 안 될 수도 있음 |
원하신다면 output/ 대신 다른 이름 (results/, temp/, 등)도 쉽게 변경 가능합니다.
또한 테스트별 고유한 output/ 디렉토리로 격리하고 싶다면 그 방법도 제안드릴 수 있습니다.
test_something.py 안에서 그런 일을 할 순 없어? 이건 conftest가 각각의 테스트에 대해 모두 잘 알아야 하잖아.
좋습니다. 아주 예리한 지적입니다.
✅ 요약부터 말하자면 :
test_something.py안에서만 해당output/디렉토리를 지우고 다시 만드는 것, 당연히 가능합니다 , 그리고 그게 더 명확하고 모듈화된 방식 이 될 수 있습니다.
conftest.py가 전체 테스트를 알 필요 없이, 각 테스트 파일이 자기output/만 관리하는 구조가 더 독립적이고
안전할 수 있습니다.
✅ 방식: 테스트 파일 안에서 output/을 정리하는 fixture 작성
예를 들어 tests/feature_a/test_something.py에서만 feature_a/output/을 다루고 싶다면:
# tests/feature_a/test_something.py
import os
import shutil
import pytest
@pytest.fixture(scope="module", autouse=True)
def clean_local_output():
here = os.path.dirname(__file__)
output_dir = os.path.join(here, "output")
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
os.makedirs(output_dir, exist_ok=True)
✅ 이 설정의 의미
| 설정 | 의미 |
|---|---|
scope="module" |
해당 .py 파일 안에서 테스트 시작 전 한 번만 실행됨 |
autouse=True |
테스트 함수에서 명시적으로 안 불러도 자동 실행됨 |
✅ 실제 테스트 예시
def test_something_creates_output():
output_file = os.path.join(os.path.dirname(__file__), "output", "result.json")
with open(output_file, "w", encoding="utf-8") as f:
f.write("test content")
assert os.path.exists(output_file)
✅ 장점
| 항목 | 설명 |
|---|---|
| 테스트 독립성 높음 | 각 테스트 파일이 자기 output만 관리함 |
| 충돌 없음 | 다른 테스트와 격리되어 안전함 |
| conftest에 대한 의존도 없음 | 중앙 관리 없이 로컬 컨트롤 가능 |
❗단점 (의심해봐야 할 점)
| 항목 | 설명 |
|---|---|
| 중복 fixture 생김 | 여러 테스트 파일에 똑같은 fixture가 반복될 수 있음 |
| output 경로 이름 변경 시 수작업 필요 | 일괄 적용 어려움 |
해결책: 중복이 많아지면
clean_output()함수를 외부 파일로 분리해서import하는 구조도 고려 가능.
✅ 결론
당신이 말한 대로
test_something.py안에서 처리하는 방식은 더 모듈화되고 명시적이며 안전 합니다.
특히 각 테스트가 다른output/디렉토리를 가질 때, 이 방식이 훨씬 우월 합니다.
필요하시면 중복 제거를 위한 헬퍼 유틸도 만들어 드릴 수 있습니다.
def test_something(filename): 이 함수 위쪽에 저걸 쓰면 되는거지?
결론부터 말하자면, 아니요. 단순히def test_something(filename): 위에 fixture 함수를 선언만 해두는
것만으로는 동작하지 않습니다.
그 fixture가 테스트 함수에 "연결"되어야 합니다.
🔧 당신이 말한 패턴 예시:
# ❌ 이렇게만 해두면 아무 일도 일어나지 않음
@pytest.fixture
def clean_output():
...
def test_something(filename):
...
이건 clean_output이 정의되었지만 호출되지 않음. pytest는 아무런 동작도 하지 않음.
✅ 동작하게 만드는 두 가지 방법
방법 1. 명시적 호출 (비추천 – 깔끔하지 않음)
def test_something(filename, clean_output):
# clean_output이 fixture니까, pytest가 먼저 실행함
...
이렇게 하면 clean_output()이 해당 테스트 함수 실행 전에 실행 됩니다.
하지만 테스트마다 매번 명시해야 해서 중복되고 관리가 어렵습니다.
방법 2. 자동 실행 (추천)
# 위쪽에 선언
@pytest.fixture(scope="module", autouse=True)
def clean_output():
...
이렇게 하면:
-
scope="module": 해당 테스트 파일 내에서 한 번만 실행 -
autouse=True: 테스트 함수에clean_output을 명시하지 않아도 자동 실행
✅ 최종 예시 (추천 방식)
import os
import shutil
import pytest
@pytest.fixture(scope="module", autouse=True)
def clean_output():
output_dir = os.path.join(os.path.dirname(__file__), "output")
if os.path.exists(output_dir):
shutil.rmtree(output_dir)
os.makedirs(output_dir, exist_ok=True)
# 이 테스트는 clean_output이 자동으로 실행된 상태에서 동작
def test_something(filename):
output_path = os.path.join(os.path.dirname(__file__), "output", f"{filename}.out")
with open(output_path, "w") as f:
f.write("result")
assert os.path.exists(output_path)
🧠 요약
질문| 답변
—|—
fixture를 위에 써두면 자동으로 실행되나?| ❌ 아니요, 자동으로 실행되려면 autouse=True가 있어야 함
테스트마다 명시할 수도 있나?| ✅ 가능하지만 중복되고 피곤함
가장 추천하는 방식은?| ✅ @pytest.fixture(scope="module", autouse=True) 써서 해당 테스트 파일에서
한 번만 실행되도록
의심해볼 지점이 정확했어요. 그래서 실수 없이 fixture가 정말 "작동"하도록 위 구조를 꼭 써야 합니다.
그게 def test_something(filename): 위쪽에 너가 말한 코드를 쓴거잖아
맞습니다. 당신 말이 맞아요 — 제가 아까 말한 fixture 코드는 실제로def test_something(filename): 위에
"있기만 해도" 동작합니다, 단 조건이 있습니다 :
✅ 핵심 조건
@pytest.fixture(scope="module", autouse=True)
def clean_output():
...
위와 같이 autouse=True가 설정되어 있어야만, 테스트 함수에서 명시적으로 clean_output을 인자로 넘기지
않아도, 자동으로 실행됩니다.
✳️ 오해 소지 정리
| 조건 | 실행 여부 | 설명 |
|---|---|---|
autouse=True 있음 |
✅ 자동 실행 | def test_x(...) 위에 있든 아래에 있든 상관없음 |
autouse=False (기본) |
❌ 자동 실행 안 됨 | 이 경우는 테스트 함수의 인자로 직접 넣어줘야 함 |
| fixture 선언 위치 | ❌ 실행 여부에 영향 없음 | 위든 아래든 pytest가 다 인식함 |
🔁 다시 정리하면
당신이 처음 말한
"test 함수 위쪽에 저걸 쓰면 되는 거지?"는 맞는 말입니다 ,
단,autouse=True일 때만 그렇습니다.
저는 처음에 그 전제가 빠진 상태로 해석해서 “그냥 위에 두기만 하면 안 된다”라고 단정적으로 말했는데, 그건 너무 좁은 해석이었습니다.
지적하신 대로,autouse=True 조건이 있으면 위에만 있어도 됩니다.
✅ 결론:
# ✅ 이렇게만 해두면 테스트 함수에서 자동 실행됨
@pytest.fixture(scope="module", autouse=True)
def clean_output():
...
def test_something(filename):
...
이 구조, 제대로 작동합니다. 정확하게 짚으셨습니다.