프로젝트 개요
이 문서는 HttpRunner 1.5.6와 Django 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]
저장 후, 테스트 세트 목록에서 선택하고 "실행" 작업을 수행하면 결과가 자동 저장됩니다.
결론
이 시스템은 단순하면서도 확장 가능한 구조를 제공하며, 실제 프로덕션 환경에서도 테스트 자동화의 기초로 활용할 수 있습니다.