[Python TDD] unittest

2026. 6. 24. 18:00·BI&Programming-Tools/Python

unittest란?

Python 표준 라이브러리에 내장된 테스트 프레임워크입니다.

별도 설치 없이 바로 사용할 수 있습니다.

Java의 JUnit에서 영향을 받아 설계됐으며, 클래스 기반으로 테스트를 구성합니다.

 

Python에서 TDD를 실천할 때 주로 쓰는 도구는 unittest와 pytest 두 가지입니다.

unittest는 표준 라이브러리라 설치 없이 쓸 수 있고 구조가 명확해서 테스트의 기본 개념을 익히기에 좋습니다.

 

실무에서는 코드가 더 간결하고 기능이 풍부한 pytest를 더 많이 쓴다고 합니다.

pytest는 unittest로 작성된 테스트도 그대로 실행할 수 있어서 서로 호환됩니다.


기본 구조

import unittest
from filter import filter_by_quality  # 테스트할 함수 import

class TestQualityFilter(unittest.TestCase):  # (1)

    def test_high_quality_passes(self):       # (2)
        read = {"seq": "ATCG", "quality": 35}
        self.assertTrue(filter_by_quality(read, threshold=30))  # (3)

if __name__ == "__main__":
    unittest.main()  # (4)

 

(1) unittest.TestCase 상속

테스트 클래스는 반드시 unittest.TestCase를 상속해야 합니다. 이를 통해 self.assert* 메서드들을 사용할 수 있게 됩니다. 클래스명은 관례상 Test로 시작합니다.

(2) test_로 시작하는 메서드

테스트 메서드는 반드시 test_로 시작해야 unittest가 테스트로 인식합니다. 이름만 보고 무엇을 테스트하는지 알 수 있게 구체적으로 짓는 게 좋습니다. test_1, test_a 같은 이름은 피합니다.

(3) self.assert* 메서드

결과를 검증하는 메서드입니다. 조건이 맞지 않으면 테스트가 실패로 기록됩니다. 단순 assert문과 달리 실패 시 더 명확한 오류 메시지를 제공합니다.

(4) unittest.main()

파일을 직접 실행할 때 테스트를 시작하는 진입점입니다. if __name__ == "__main__" 안에 두는 게 관례입니다.


실행 방법

# 파일 직접 실행
python test_filter.py

# unittest 모듈로 실행
python -m unittest test_filter

# 현재 디렉토리에서 test_*.py 파일 자동 탐색 후 실행
python -m unittest discover

# 결과를 자세히 출력
python -m unittest test_filter -v

 



자주 쓰는 assert 메서드

메소드 설명
assertEqual(a, b) a == b 인지 확인합니다.
assertNotEqual(a, b) a != b 인지 확인합니다.
assertTrue(x) x가 참인지 확인합니다.
assertFalse(x) x가 거짓인지 확인합니다.
assertIsNone(x) x is None 인지 확인합니다.
assertIsNotNone(x) x is not None 인지 확인합니다.
assertIn(a, b) a in b 인지 확인합니다.
assertNotIn(a, b) a not in b 인지 확인합니다.
assertRaises(Error, ...) 특정 예외가 발생하는지 확인합니다.
self.assertEqual(result, 5)
self.assertIn("ATG", sequence)
self.assertIsNone(result)

# 예외 발생 확인 — context manager 방식이 더 권장됩니다
with self.assertRaises(ValueError):
    parse_sequence("")

 

가장 많이 쓰는 건 assertEqual입니다.

반환값이 기대한 값과 같은지 확인하는 경우가 대부분이기 때문입니다.


 

TDD 실전 예시 — FASTQ 품질 필터링

FASTQ 파일에서 품질 점수(Phred score)가 임계값 이상인 read만 통과시키는 함수를 TDD로 만드는 예시입니다.

🔴 Red — 테스트 먼저 작성

구현보다 먼저 "이 함수가 어떻게 동작해야 하는가"를 테스트로 정의합니다.

어떤 입력에 어떤 결과를 반환해야 하는지, 경계값은 어떻게 처리해야 하는지를 생각하면서 작성합니다.

# test_filter.py
import unittest
from filter import filter_by_quality  # 아직 존재하지 않는 함수

class TestQualityFilter(unittest.TestCase):

    def test_high_quality_read_passes(self):
        """품질 점수가 임계값보다 높으면 통과해야 합니다."""
        read = {"seq": "ATCG", "quality": 35}
        self.assertTrue(filter_by_quality(read, threshold=30))

    def test_low_quality_read_fails(self):
        """품질 점수가 임계값보다 낮으면 걸러야 합니다."""
        read = {"seq": "ATCG", "quality": 15}
        self.assertFalse(filter_by_quality(read, threshold=30))

    def test_exact_threshold_passes(self):
        """품질 점수가 임계값과 정확히 같으면 통과해야 합니다."""
        read = {"seq": "ATCG", "quality": 30}
        self.assertTrue(filter_by_quality(read, threshold=30))

if __name__ == "__main__":
    unittest.main()
python test_filter.py
# ModuleNotFoundError: No module named 'filter'
# ← 예상된 실패입니다. filter.py가 없으니 당연합니다.

테스트 메서드 안에 docstring을 달아두면 -v 옵션 실행 시 어떤 테스트인지 설명이 같이 출력됩니다.

🟢 Green — 테스트를 통과하는 최소 코드 작성

완벽한 코드가 아니어도 됩니다. 지금 작성한 테스트 3개를 통과시키는 것만 목표로 합니다.

# filter.py
def filter_by_quality(read, threshold=20):
    return read["quality"] >= threshold
python test_filter.py
# ...
# Ran 3 tests in 0.001s
# OK

🔵 Refactor — 코드 정리 + 테스트 보강

통과한 상태에서 더 나은 코드로 다듬습니다. 리팩토링 후에도 기존 테스트 3개는 계속 통과해야 합니다.

예외 처리를 추가하고, 그에 맞는 테스트도 함께 추가합니다. 테스트 없이 코드만 개선하면 TDD가 아닙니다.

# filter.py — 리팩토링 후
def filter_by_quality(read, threshold=20):
    if not isinstance(read, dict):
        raise TypeError("read는 딕셔너리여야 합니다.")
    if "quality" not in read:
        raise ValueError("read에 'quality' 키가 없습니다.")
    return read["quality"] >= threshold
# test_filter.py — 테스트 보강
def test_non_dict_input_raises_type_error(self):
    """딕셔너리가 아닌 입력은 TypeError를 발생시켜야 합니다."""
    with self.assertRaises(TypeError):
        filter_by_quality("잘못된입력", threshold=30)

def test_missing_quality_key_raises_value_error(self):
    """quality 키가 없으면 ValueError를 발생시켜야 합니다."""
    read = {"seq": "ATCG"}  # quality 키 없음
    with self.assertRaises(ValueError):
        filter_by_quality(read, threshold=30)
python test_filter.py
# Ran 5 tests in 0.001s
# OK

 

 

setUp / tearDown

테스트 메서드마다 동일한 데이터를 반복해서 만드는 경우, setUp으로 한 곳에 모아두면 코드가 깔끔해집니다.

class TestQualityFilter(unittest.TestCase):

    def setUp(self):
        """각 테스트 메서드 실행 전에 자동으로 호출됩니다."""
        self.high_q_read = {"seq": "ATCG", "quality": 35}
        self.low_q_read  = {"seq": "ATCG", "quality": 15}
        self.threshold   = 30

    def tearDown(self):
        """각 테스트 메서드 실행 후에 자동으로 호출됩니다."""
        # 파일 핸들, DB 연결 등 테스트 후 정리가 필요할 때 씁니다.
        # 간단한 데이터만 쓰는 경우엔 보통 비워둡니다.
        pass

    def test_high_quality_passes(self):
        self.assertTrue(filter_by_quality(self.high_q_read, threshold=self.threshold))

    def test_low_quality_fails(self):
        self.assertFalse(filter_by_quality(self.low_q_read, threshold=self.threshold))

 

setUp은 테스트 메서드 하나가 실행될 때마다 새로 호출됩니다.

즉 테스트끼리 데이터를 공유하지 않습니다. 이게 FIRST 원칙의 Independent를 지키는 방법입니다.

한 테스트가 데이터를 바꿔도 다른 테스트에 영향을 주지 않습니다.


 

파일 구조 관례

project/
├── filter.py          # 실제 코드
└── test_filter.py     # 테스트 코드

 

테스트 파일이 많아지면 디렉토리로 분리합니다.

project/
├── src/
│   └── filter.py
└── tests/
    └── test_filter.py
  • 테스트 파일명은 test_로 시작합니다. (test_filter.py)
  • 테스트 클래스명은 Test로 시작합니다. (TestQualityFilter)
  • 테스트 메서드명은 test_로 시작하고, 무엇을 테스트하는지 구체적으로 씁니다. (test_low_quality_read_fails)

'BI&Programming-Tools > Python' 카테고리의 다른 글

[Python TDD] Mocking  (0) 2026.06.24
[Python TDD] pytest  (0) 2026.06.24
TDD 개념 — Test Driven Development  (0) 2026.06.24
Python 커맨드 라인 인자 받기 — sys.argv & argparse  (0) 2026.06.23
[파이썬 기초] 차원 축소 (PCA, UMAP)  (0) 2026.06.21
'BI&Programming-Tools/Python' 카테고리의 다른 글
  • [Python TDD] Mocking
  • [Python TDD] pytest
  • TDD 개념 — Test Driven Development
  • Python 커맨드 라인 인자 받기 — sys.argv & argparse
데이터로 읽는 생명
데이터로 읽는 생명
is-note 님의 블로그 입니다.
  • 데이터로 읽는 생명
    In Silico Note
    데이터로 읽는 생명
  • 전체
    오늘
    어제
    • 분류 전체보기 (190)
      • Bio-Knowledge (16)
        • 분자생물학 & 유전학 기초 (0)
        • 전사체학 & 유전자 발현 (0)
        • 구조생물학 & 단백질 (0)
        • 싱글셀 & 다중오믹스 (0)
        • 임상유전학 & 질환 데이터 (0)
      • Programming (4)
        • API (4)
      • BI&Programming-Tools (111)
        • File Formats (14)
        • Linux & Bash Script (38)
        • Python (35)
        • R (11)
        • 통계 (8)
        • Pipeline Manager (0)
        • Etc (5)
      • Bio Data Analysis (28)
        • 서열분석개론 (6)
        • WGS(Whole Genome Seq) (11)
        • WES(Whole Exome Seq) (2)
        • RNA-Seq (4)
        • Metagenome (2)
        • Non-human Resequencing (1)
        • 임상유전체 분석 (1)
        • Multi-Omics (1)
      • Bio-Trends & Tech (1)
      • 코딩테스트 연습 (30)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    생물정보학
    R기초
    데이터분석
    리눅스
    통계
    GATK
    wgs
    파이썬기초
    파이썬연습
    ngs
    유전체분석
    리눅스기초
    FASTQ
    파이썬
    bioinformatics
    분자생물학
    fasta
    R
    파일포맷
    코딩테스트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
데이터로 읽는 생명
[Python TDD] unittest
상단으로

티스토리툴바