본문 바로가기
잡(job)기술/파이썬 공부

당신의 테스트는 안녕한가요? Pytest로 데이터베이스를 제대로 검증하는 3가지 핵심 원리

by 무니이구나 2026. 1. 2.

 

 

들어가며

금요일 오후, 간단한 스키마 변경 후 배포했는데 월요일 아침에 데이터 불일치 버그가 쏟아진 경험이 있을 것이다. 이런 문제는 대부분 테스트의 신뢰도 부족에서 시작된다. 특히 데이터베이스 구조 변경은 코드 전체에 예상치 못한 영향을 준다. 이 글에서는 pytest와 SQLite를 기준으로, 데이터베이스 구조를 자동으로 검증하는 세 가지 핵심 원리를 정리한다. 이 원리들을 적용하면 스키마 변경이 훨씬 덜 두려워진다.

참고: 본 글의 예제는 SQLite 기준이다. PostgreSQL이나 MySQL에서는 스키마 조회 방식이 다르다.


1. 테스트는 항상 같은 출발선에서 시작한다: @pytest.fixture

견고한 테스트의 첫 번째 원칙은 격리다. 각 테스트는 이전 테스트의 결과에 영향을 받아서는 안 된다. pytest에서는 @pytest.fixture를 사용해 이 문제를 해결한다. fixture는 테스트 실행 전의 준비(setup)와 실행 후의 정리(teardown)를 담당한다. 이때 핵심은 yield다.

fixture 동작 흐름

  1. fixture가 실행된다.
  2. 기존 DB 파일이 있으면 삭제한다.
  3. init_db()를 호출해 새로운 데이터베이스를 만든다.
  4. yield에서 테스트 함수로 제어가 넘어간다.
  5. 테스트가 실행된다.
  6. 테스트 성공/실패와 관계없이 다시 fixture로 돌아온다.
  7. DB 파일을 삭제하고 정리한다.

예제: 데이터베이스 fixture

import os
import sqlite3
import pytest

DB_PATH = "test.db"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()
    cursor.execute("""
        CREATE TABLE auth_tokens (
            token TEXT NOT NULL,
            user_id INTEGER NOT NULL
        )
    """)
    conn.commit()
    conn.close()

@pytest.fixture
def setup_database():
    if os.path.exists(DB_PATH):
        os.remove(DB_PATH)

    init_db()
    yield

    if os.path.exists(DB_PATH):
        os.remove(DB_PATH)

 

이 방식의 핵심은 단순하다.

테스트마다 항상 새 데이터베이스를 만든다

 

이렇게 하면 테스트 순서나 실행 횟수에 따라 결과가 달라지는 문제를 원천적으로 막을 수 있다.


2. 데이터가 아니라 설계도를 검증한다: PRAGMA table_info

많은 테스트는 데이터 삽입과 조회만 확인한다. 하지만 스키마가 잘못되면 데이터 테스트 자체가 의미 없어질 수 있다. SQLite에서는 PRAGMA table_info(테이블명)을 사용해 테이블 구조를 조회할 수 있다. 이 명령은 각 컬럼의 정보를 행(row) 단위로 반환한다. 중요한 값은 다음 두 가지다.

인덱스 의미
row[1] 컬럼 이름
row[2] 컬럼 타입

 

이 결과를 딕셔너리로 변환하면 검증이 쉬워진다.

columns = {row[1]: row[2] for row in cursor.fetchall()}

실제 스키마 검증 테스트

def test_auth_tokens_table_schema(setup_database):
    conn = sqlite3.connect(DB_PATH)
    cursor = conn.cursor()

    cursor.execute("PRAGMA table_info(auth_tokens)")
    columns = {row[1]: row[2] for row in cursor.fetchall()}

    assert "token" in columns
    assert columns["token"] == "TEXT"
    assert "user_id" in columns
    assert columns["user_id"] == "INTEGER"

    conn.close()

 

이 테스트는 다음을 자동으로 검증한다.

  • auth_tokens 테이블이 존재하는가?
  • token 컬럼이 있는가?
  • 타입이 TEXT인가?

❌ 실패 사례 예시

만약 실수로 스키마를 이렇게 바꿨다면,

token INTEGER

 

테스트 결과는 다음과 같이 실패한다.

AssertionError: assert 'INTEGER' == 'TEXT'

 

이 실패는 배포 전에 바로 감지된다. 이것이 스키마 테스트의 가장 큰 가치다.


3. 필요한 것만 선언한다: pytest의 의존성 주입

pytest에서는 fixture를 직접 호출하지 않는다. 테스트 함수의 파라미터로 선언하기만 하면 된다.

def test_auth_tokens_table_schema(setup_database):
    ...

 

pytest는 이 함수를 실행하기 전에 다음을 자동으로 처리한다.

  1. setup_database fixture 실행
  2. 데이터베이스 준비
  3. 테스트 실행
  4. 정리 작업 수행

이 방식이 바로 의존성 주입이다. 개발자는 “이 테스트에는 이 환경이 필요하다”라고 선언만 하면 된다. 준비 과정은 프레임워크가 책임진다.


마무리

이 글에서 소개한 세 가지 원리는 하나의 흐름으로 연결된다.

  1. @pytest.fixture가 테스트 환경을 완전히 격리하고
  2. PRAGMA table_info가 데이터베이스 설계도를 직접 검증하며
  3. pytest의 의존성 주입이 이 과정을 간결하게 만든다.

이 테스트가 궁극적으로 답해주는 질문은 하나다.

init_db 함수가 정말 의도한 대로 테이블과 컬럼을 만들고 있는가?

 

이 질문에 자동으로 답해주는 테스트가 있다면, 스키마 변경은 더 이상 공포가 아니다. 이제 자신의 프로젝트를 점검해 보자. 데이터베이스 구조를 자동으로 검증하는 테스트가 준비되어 있는가?