Django Admin과 HttpRunner 1.5.6를 활용한 간편한 API 테스트 플랫폼 구축

프로젝트 개요

이 문서는 HttpRunner 1.5.6Django Admin을 기반으로 한 간단한 API 테스트 관리 시스템의 설계 및 구현 방법을 설명합니다. 사용자는 웹 인터페이스를 통해 테스트 케이스를 생성하고, 실행하며 결과를 확인할 수 있습니다.

환경 설정 및 의존성 설치

먼저 필요한 라이브러리를 설치합니다.

pip install httprunner==1.5.6 -i https://pypi.doubanio.com/simple/

데이터 모델 설계

테스트 플랫폼은 다음과 같은 주요 엔티티로 구성됩니다:

  • Project: 테스트 프로젝트 정보 (이름, 생성/수정 시간)
  • TestSuite: 하나의 YAML 파일에 해당하는 테스트 세트 (소속 프로젝트, 기본 도메인, 요청 설정, 변수, 테스트 항목 포함)
  • TestCase: 각 테스트 케이스 (스킵 여부, 요청 데이터, 검증 조건, 데이터 추출 설정)
  • TestResult: 테스트 실행 결과 (성공 여부, 소요 시간, 통계 정보, 상세 로그 등)

YAML 형식 필드 커스터마이징

Django의 기본 필드는 문자열만 저장 가능하지만, 테스트 설정에는 딕셔너리 및 리스트가 필요합니다. 이를 위해 YamlField 클래스를 정의하여 자동 직렬화/역직렬화를 처리합니다.

from django.db import models
import yaml

class YamlField(models.TextField):
    def to_python(self, value):
        if not value:
            return {}
        if isinstance(value, (dict, list)):
            return value
        return yaml.safe_load(value)

    def get_prep_value(self, value):
        return value if value is None else yaml.dump(value, default_flow_style=False)

    def from_db_value(self, value, expression, connection):
        return self.to_python(value)

공통 속성 추상화

모든 엔티티에 공통적으로 적용되는 이름, 생성일, 수정일 필드를 재사용하기 위해 추상 모델을 정의합니다.

class ModelWithName(models.Model):
    class Meta:
        abstract = True

    name = models.CharField("이름", max_length=200)
    created = models.DateTimeField("생성일시", auto_now_add=True)
    modified = models.DateTimeField("최종 수정일시", auto_now=True)

    def __str__(self):
        return self.name

모델 정의

각 엔티티에 대한 모델을 작성합니다.

class Project(ModelWithName):
    class Meta:
        verbose_name = "프로젝트"
        verbose_name_plural = "프로젝트"

class TestSuite(ModelWithName):
    project = models.ForeignKey(Project, on_delete=models.CASCADE, related_name='suites', verbose_name="프로젝트")
    base_url = models.CharField("기본 도메인", max_length=500, blank=True, null=True)
    request = YamlField("기본 요청 설정", blank=True)
    variables = YamlField("사용자 변수", blank=True)

    @property
    def data(self):
        config_request = self.request.copy()
        config_request['base_url'] = self.base_url
        return {
            'name': self.name,
            'config': {'request': config_request, 'variables': self.variables},
            'api': {},
            'testcases': [tc.data for tc in self.tests.all()]
        }

    def run(self):
        from httprunner.task import HttpRunner
        runner = HttpRunner().run([self.data])
        summary = runner.summary
        if summary:
            time_info = summary['time']
            stat_info = summary['stat']
            TestResult.objects.create(
                suite=self,
                success=summary['success'],
                start_at=datetime.datetime.fromtimestamp(time_info['start_at']),
                duration=datetime.timedelta(seconds=time_info['duration']),
                test_run=stat_info['testsRun'],
                successes=stat_info['successes'],
                skipped=stat_info['skipped'],
                failures=stat_info['failures'],
                errors=stat_info['errors'],
                expected_failures=stat_info['expectedFailures'],
                unexpected_successes=stat_info['unexpectedSuccesses'],
                platform=json.dumps(summary['platform'], indent=2, ensure_ascii=False),
                details=summary['details']
            )
        return summary

class TestCase(ModelWithName):
    suite = models.ForeignKey(TestSuite, on_delete=models.CASCADE, related_name='tests', verbose_name="테스트 세트")
    skip = models.BooleanField("건너뛰기", default=False)
    request = YamlField("요청 정보")
    extract = YamlField("응답 추출", blank=True)
    validate = YamlField("검증 조건", blank=True)

    @property
    def data(self):
        return {
            'name': self.name,
            'skip': self.skip,
            'request': self.request,
            'extract': self.extract,
            'validate': self.validate
        }

class TestResult(models.Model):
    suite = models.ForeignKey(TestSuite, on_delete=models.CASCADE, related_name='results', verbose_name="테스트 세트")
    success = models.BooleanField("성공 여부")
    start_at = models.DateTimeField("실행 시작 시점")
    duration = models.DurationField("실행 소요 시간")
    platform = models.TextField("실행 환경 정보")
    test_run = models.SmallIntegerField("총 실행 수")
    successes = models.SmallIntegerField("성공 수")
    skipped = models.SmallIntegerField("건너뛴 수")
    failures = models.SmallIntegerField("실패 수")
    errors = models.SmallIntegerField("오류 수")
    expected_failures = models.SmallIntegerField("예상 실패 수")
    unexpected_successes = models.SmallIntegerField("비예상 성공 수")
    details = models.TextField("상세 결과")
    created = models.DateTimeField("기록 일시", auto_now_add=True)

    def __str__(self):
        return f"{self.suite.name} - 결과"

Django Admin 설정

Admin 인터페이스에서 테스트 세트와 케이스를 연관하여 편집할 수 있도록 구성합니다.

from django.contrib import admin
from .models import Project, TestSuite, TestCase, TestResult

@admin.register(Project)
class ProjectAdmin(admin.ModelAdmin):
    list_display = ('name', 'created', 'modified')

class TestCaseInline(admin.StackedInline):
    model = TestCase
    extra = 1

@admin.register(TestSuite)
class TestSuiteAdmin(admin.ModelAdmin):
    inlines = [TestCaseInline]
    list_display = ('name', 'project', 'base_url', 'created', 'modified')
    list_filter = ('project',)
    actions = ['execute_suite']

    def execute_suite(self, request, queryset):
        for suite in queryset:
            suite.run()
    execute_suite.short_description = "선택된 세트 실행"

@admin.register(TestResult)
class TestResultAdmin(admin.ModelAdmin):
    readonly_fields = (
        'suite', 'success', 'start_at', 'duration', 'platform',
        'test_run', 'successes', 'skipped', 'failures', 'errors',
        'expected_failures', 'unexpected_successes', 'details', 'created'
    )
    fields = (
        ('suite', 'success'),
        ('start_at', 'duration'),
        ('platform',),
        ('test_run', 'successes', 'skipped', 'failures', 'errors', 'expected_failures', 'unexpected_successes'),
        ('details',)
    )
    list_display = ('suite', 'success', 'test_run', 'successes', 'errors', 'failures', 'start_at', 'duration')
    list_filter = ('suite',)

테스트 실행 예시

관리자 페이지에서 다음 값을 입력해 보세요:

  • 기본 요청 설정:
    headers:
    x-text: abc123
  • 변수 설정:
    a: 1
    b: 2
  • 요청 데이터:
    url: /get
    method: GET
    params:
    a: $a
    b: $b
  • 응답 추출:
    - res_url: content.url
  • 검증 조건:
    - eq: [status_code, 200]

저장 후, 테스트 세트 목록에서 선택하고 "실행" 작업을 수행하면 결과가 자동 저장됩니다.

결론

이 시스템은 단순하면서도 확장 가능한 구조를 제공하며, 실제 프로덕션 환경에서도 테스트 자동화의 기초로 활용할 수 있습니다.

태그: Django HttpRunner API Testing YAML database modeling

6월 8일 16:48에 게시됨