设计一个项目管理模块
项目管理模块,用于管理创建的项目,每个项目都有自己独特性,因此,项目管理模块,相当于一个存储仓库,用于存储接口等信息。
定义日志
定义一个日志,用于所有模块的debug,在settings里设置:
# 可以在全局配置settings.py中的LOGGING,来配置日志信息
LOGGING = {
# 版本号
'version': 1,
# 指定是否禁用已经存在的日志器
'disable_existing_loggers': False,
# 日志的显示格式
'formatters': {
# simple为简化版格式的日志
'simple': {
'format': '%(asctime)s - [%(levelname)s] - [msg]%(message)s'
},
# verbose为详细格式的日志
'verbose': {
'format': '%(asctime)s - [%(levelname)s] - %(name)s - [msg]%(message)s - [%(filename)s:%(lineno)d ]'
},
},
# filters指定日志过滤器
'filters': {
'require_debug_true': {
'()': 'django.utils.log.RequireDebugTrue',
},
},
# handlers指定日志输出渠道
'handlers': {
# console指定输出到控制台
'console': {
'level': 'DEBUG',
'filters': ['require_debug_true'],
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
# 日志保存到日志文件
'file': {
'level': 'DEBUG',
'class': 'logging.handlers.RotatingFileHandler',
# 指定存放日志文件的所处路径
'filename': os.path.join(BASE_DIR, "logs/test.log"), # 日志文件的位置
'maxBytes': 100 * 1024 * 1024,
'backupCount': 10,
'formatter': 'verbose',
'encoding': 'utf-8'
},
},
# 定义日志器
'loggers': {
'mytest': { # 定义了一个名为mytest的日志器
'handlers': ['console', 'file'],
'propagate': True,
'level': 'DEBUG', # 日志器接收的最低日志级别
},
}
}
根据配置路径logs/test.log,在工程目录下创建存放日志的文件夹logs。
创建应用并注册
django-admin startapp projects
拖动到apps后,在INSTALLED_APPS注册。
设计模型
首先在utils文件夹里创建base_models.py,定义一个基础模型:
from django.db import models
class BaseModel(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间', help_text='创建时间')
update_time = models.DateTimeField(auto_now=True, verbose_name='更新时间', help_text='更新时间')
class Meta:
# 指定在迁移时不创建表
abstract = True
然后在projects应用里的models.py,继承基础模型并定义一个模型:
from django.db import models
from utils.base_models import BaseModel
# Create your models here.
class Projects(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('项目名称', max_length=200, unique=True, help_text='项目名称')
leader = models.CharField('负责人', max_length=50, help_text='项目负责人')
tester = models.CharField('测试人员', max_length=50, help_text='项目测试人员')
programmer = models.CharField('开发人员', max_length=50, help_text='开发人员')
publish_app = models.CharField('发布应用', max_length=100, help_text='发布应用')
desc = models.CharField('简要描述', max_length=200, null=True, blank=True, default='', help_text='简要描述')
class Meta:
db_table = 'tb_projects'
verbose_name = '项目信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
因为涉及到一些其他逻辑,现在我们补充一下其他应用:
1、创建interfaces应用,并定义models、serializers:
# models
from django.db import models
from utils.base_models import BaseModel
class Interfaces(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('接口名称', max_length=200, unique=True, help_text='接口名称')
project = models.ForeignKey('projects.Projects', on_delete=models.CASCADE,
related_name='interfaces', help_text='所属项目')
tester = models.CharField('测试人员', max_length=50, help_text='测试人员')
desc = models.CharField('简要描述', max_length=200, null=True, blank=True, help_text='简要描述')
class Meta:
db_table = 'tb_interfaces'
verbose_name = '接口信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
# seriializers
from rest_framework import serializers
from .models import Interfaces
from projects.models import Projects
from testcases.models import Testcases
from configures.models import Configures
from utils import common
from utils import validates
class InterfacesModelSerializer(serializers.ModelSerializer):
project = serializers.StringRelatedField(label='所属项目名称', help_text='所属项目名称')
# project_id = serializers.PrimaryKeyRelatedField(queryset=Projects.objects.all(),
# label='项目id', help_text='项目id',
# write_only=True)
project_id = serializers.PrimaryKeyRelatedField(queryset=Projects.objects.all(),
label='项目id', help_text='项目id',
)
class Meta:
model = Interfaces
fields = ('id', 'name', 'tester', 'create_time', 'desc', 'project', 'project_id')
extra_kwargs = {
'create_time': {
'read_only': True,
'format': common.datetime_fmt()
}
}
def create(self, validated_data):
project = validated_data.pop('project_id')
validated_data['project'] = project
return super().create(validated_data)
def update(self, instance, validated_data):
if 'project_id' in validated_data:
project = validated_data.pop('project_id')
validated_data['project'] = project
return super().update(instance, validated_data)
class TestcasesNamesModelSerializer(serializers.ModelSerializer):
class Meta:
model = Testcases
fields = ('id', 'name')
class TestcasesByInterfaceIdModelSerializer(serializers.ModelSerializer):
testcases = TestcasesNamesModelSerializer(many=True, read_only=True)
class Meta:
model = Interfaces
fields = ('testcases', )
class ConfiguresNamesModelSerializer(serializers.ModelSerializer):
class Meta:
model = Configures
fields = ('id', 'name')
class ConfiguresByInterfaceIdModelSerializer(serializers.ModelSerializer):
configures = ConfiguresNamesModelSerializer(many=True, read_only=True)
class Meta:
model = Interfaces
fields = ('configures', )
class InterfaceRunSerializer(serializers.ModelSerializer):
"""
通过接口来运行测试用例序列化器
"""
env_id = serializers.IntegerField(write_only=True,
help_text='环境变量ID',
validators=[validates.is_exised_env_id])
class Meta:
model = Interfaces
fields = ('id', 'env_id')
2、创建应用testcases并设计models
from django.db import models
from utils.base_models import BaseModel
class Testcases(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('用例名称', max_length=50, unique=True, help_text='用例名称')
interface = models.ForeignKey('interfaces.Interfaces', on_delete=models.CASCADE, related_name='testcases',
help_text='所属接口')
# include = models.ForeignKey('', on_delete=models.SET_NULL, null=True, related_name='testcases')
include = models.TextField('前置', null=True, help_text='用例执行前置顺序')
author = models.CharField('编写人员', max_length=50, help_text='编写人员')
request = models.TextField('请求信息', help_text='请求信息')
class Meta:
db_table = 'tb_testcases'
verbose_name = '用例信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
3、创建应用configures并设计models
from django.db import models
from utils.base_models import BaseModel
class Configures(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('配置名称', max_length=50, help_text='配置名称')
interface = models.ForeignKey('interfaces.Interfaces',
on_delete=models.CASCADE,
related_name='configures',
help_text='所属接口')
author = models.CharField('编写人员', max_length=50, help_text='编写人员')
request = models.TextField('请求信息', help_text='请求信息')
class Meta:
db_table = 'tb_configures'
verbose_name = '配置信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
4、在utils添加common、validates模块
# common
from datetime import datetime
from rest_framework.response import Response
import yaml
from httprunner.task import HttpRunner
from httprunner.report import render_html_report
from debugtalks.models import DebugTalks
from configures.models import Configures
from testcases.models import Testcases
from reports.models import Reports
def datetime_fmt():
locale.setlocale(locale.LC_CTYPE, 'chinese')
return '%Y年%m月%d日 %H:%M:%S'
def create_report(runner, report_name=None):
"""
创建测试报告
:param runner:
:param report_name:
:return:
"""
time_stamp = int(runner.summary["time"]["start_at"])
start_datetime = datetime.fromtimestamp(time_stamp).strftime('%Y-%m-%d %H:%M:%S')
runner.summary['time']['start_datetime'] = start_datetime
# duration保留3位小数
runner.summary['time']['duration'] = round(runner.summary['time']['duration'], 3)
report_name = report_name if report_name else start_datetime
runner.summary['html_report_name'] = report_name
for item in runner.summary['details']:
# 对时间戳进行处理
try:
time_stamp = int(item['time']['start_at'])
detail['time']['start_at'] = datetime.fromtimestamp(time_stamp).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
pass
try:
for record in item['records']:
# 对时间戳进行处理
try:
time_stamp = int(record['meta_data']['request']['start_timestamp'])
record['meta_data']['request']['start_timestamp'] = \
datetime.fromtimestamp(time_stamp).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
pass
record['meta_data']['response']['content'] = record['meta_data']['response']['content']. \
decode('utf-8')
record['meta_data']['response']['cookies'] = dict(record['meta_data']['response']['cookies'])
request_body = record['meta_data']['request']['body']
if isinstance(request_body, bytes):
record['meta_data']['request']['body'] = request_body.decode('utf-8')
except Exception as e:
continue
summary = json.dumps(runner.summary, ensure_ascii=False)
report_name = report_name + '_' + datetime.strftime(datetime.now(), '%Y%m%d%H%M%S')
report_path = runner.gen_html_report(html_report_name=report_name)
with open(report_path, encoding='utf-8') as stream:
reports = stream.read()
test_report = {
'name': report_name,
'result': runner.summary.get('success'),
'success': runner.summary.get('stat').get('successes'),
'count': runner.summary.get('stat').get('testsRun'),
'html': reports,
'summary': summary
}
report_obj = Reports.objects.create(**test_report)
return report_obj.id
def generate_testcase_file(instance, env, testcase_dir_path):
testcase_list = []
config = {
'config': {
'name': instance.name,
'request': {
'base_url': env.base_url if env else ''
}
}
}
testcase_list.append(config)
# 获取include信息
include = json.loads(instance.include, encoding='utf-8')
# 获取request字段
request = json.loads(instance.request, encoding='utf-8')
# 获取用例所属接口名称
interface_name = instance.interface.name
# 获取用例所属项目名称
project_name = instance.interface.project.name
testcase_dir_path = os.path.join(testcase_dir_path, project_name)
if not os.path.exists(testcase_dir_path):
os.makedirs(testcase_dir_path)
# 生成debugtalk.py文件,放到项目根目录下
debugtalk_obj = DebugTalks.objects.filter(project__name=project_name).first()
debugtalk = debugtalk_obj.debugtalk if debugtalk_obj else ''
with open(os.path.join(testcase_dir_path, 'debugtalk.py'), 'w', encoding='utf-8') as f:
f.write(debugtalk)
testcase_dir_path = os.path.join(testcase_dir_path, interface_name)
if not os.path.exists(testcase_dir_path):
os.makedirs(testcase_dir_path)
# {"config":1,"testcases":[1,2,3]}
if 'config' in include:
config_id = include.get('config')
config_obj = Configures.objects.filter(id=config_id).first()
if config_obj:
config_request = json.loads(config_obj.request, encoding='utf-8')
config_request['config']['request']['base_url'] = env.base_url if env else ''
testcase_list[0] = config_request
# 处理前置用例
if 'testcases' in include:
for testcase_id in include.get('testcases'):
testcase_obj = Testcases.objects.filter(id=testcase_id).first()
try:
testcase_request = json.loads(testcase_obj.request, encoding='utf-8')
except Exception as e:
continue
testcase_list.append(testcase_request)
# 把当前需要执行的用例追加到testcase_list最后
testcase_list.append(request)
# with open(os.path.join(testcase_dir_path, instance.name + '.yaml'), 'w', encoding='utf-8') as f:
# yaml.dump(testcase_list, f, all_unicode=True)
with open(os.path.join(testcase_dir_path, instance.name + '.yaml'), 'w', encoding='utf-8') as f:
yaml.dump(testcase_list, f, allow_unicode=True)
def run_testcase(instance, testcase_dir_path):
# 1、运行用例
runner = HttpRunner()
try:
runner.run(testcase_dir_path)
except Exception as e:
res = {'ret': False, 'msg': '用例执行失败'}
return Response(res, status=400)
# 2、创建报告
report_id = create_report(runner, instance.name)
# 3、用例运行成功之后,需要把生成的报告id返回
data = {
'id': report_id
}
return Response(data, status=201)
为common安装一些依赖组件:
pip install djangorestframework-yaml==2.0.0
pip install httprunner==1.5.8
# validates
from rest_framework import serializers
from projects.models import Projects
from interfaces.models import Interfaces
from envs.models import Envs
def is_exised_project_id(value):
"""
校验项目id是否存在
:param value:
:return:
"""
if not Projects.objects.filter(id=value).exists():
raise serializers.ValidationError('项目id不存在')
def is_exised_interface_id(value):
"""
校验接口id是否存在
:param value:
:return:
"""
if not Interfaces.objects.filter(id=value).exists():
raise serializers.ValidationError('接口id不存在')
def is_exised_env_id(value):
"""
校验环境变量id是否存在
:param value:
:return:
"""
if not Envs.objects.filter(id=value).exists():
raise serializers.ValidationError('环境变量ID不存在')
5、创建应用debugtalks并设计models
from django.db import models
from utils.base_models import BaseModel
class DebugTalks(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('debugtalk文件名称', max_length=200, default='debugtalk.py', help_text='debugtalk文件名称')
debugtalk = models.TextField(null=True, default='#debugtalk.py', help_text='debugtalk.py文件')
project = models.OneToOneField('projects.Projects', on_delete=models.CASCADE,
related_name='debugtalks', help_text='所属项目')
class Meta:
db_table = 'tb_debugtalks'
verbose_name = 'debugtalk.py文件'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
6、创建应用reports并设计models
from django.db import models
from utils.base_models import BaseModel
class Reports(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('报告名称', max_length=200, unique=True, help_text='报告名称')
result = models.BooleanField('执行结果', default=1, help_text='执行结果') # 1为成功, 0为失败
count = models.IntegerField('用例总数', help_text='总用例数')
success = models.IntegerField('成功总数', help_text='成功总数')
html = models.TextField('报告HTML源码', help_text='报告HTML源码', null=True, blank=True, default='')
summary = models.TextField('报告详情', help_text='报告详情', null=True, blank=True, default='')
class Meta:
db_table = 'tb_reports'
verbose_name = '测试报告'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
7、创建应用envs并设计models
from django.db import models
from utils.base_models import BaseModel
class Envs(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField(verbose_name='环境名称', max_length=200, unique=True, help_text='环境名称')
base_url = models.URLField(verbose_name='请求base url', max_length=200, help_text='请求base url')
desc = models.CharField(verbose_name='简要描述', max_length=200, help_text='简要描述')
class Meta:
db_table = 'tb_envs'
verbose_name = '环境信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
8、创建应用testsuits并设计models
from django.db import models
from utils.base_models import BaseModel
class Testsuits(BaseModel):
id = models.AutoField(verbose_name='id主键', primary_key=True, help_text='id主键')
name = models.CharField('套件名称', max_length=200, unique=True, help_text='套件名称')
project = models.ForeignKey('projects.Projects', on_delete=models.CASCADE,
related_name='testsuits', help_text='所属项目')
# include = models.TextField(null=False)
include = models.TextField('包含的接口', help_text='包含的接口')
class Meta:
db_table = 'tb_testsuits'
verbose_name = '套件信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
执行迁移并运行
没有问题后继续。
设计接口
import json
import logging
import os
from datetime import datetime
from django_filters.rest_framework import DjangoFilterBackend
from django.db.models import Count
from django.conf import settings
# from rest_framework.filters import OrderingFilter
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework import permissions
from rest_framework.response import Response
from .models import Projects
from interfaces.models import Interfaces
from configures.models import Configures
from testsuits.models import Testsuits
from testcases.models import Testcases
from envs.models import Envs
from .serializers import ProjectsModelSerializer, ProjectsNamesModelSerializer, \
InterfacesByProjectIdModelSerializer, ProjectsRunSerializer
from utils import common
# from utils.pagination import MyPagination
# 定义日志器用于记录日志,logging.getLogger('全局配置settings.py中定义的日志器名')
logger = logging.getLogger('mytest')
class ProjectsViewSet(viewsets.ModelViewSet):
"""
list:
获取项目的列表信息
retrive:
获取项目详情数据
create:
创建项目
names:
获取项目名称
interfaces:
获取某个项目下的接口名称
"""
queryset = Projects.objects.all()
serializer_class = ProjectsModelSerializer
permission_classes = [permissions.IsAuthenticated]
# 如果父类中有提供相关的逻辑
# 1、绝大部分不需要修改,只有少量要修改的,直接对父类中的action进行拓展
# 2、绝大部分都需要修改的话,那么直接自定义即可
def list(self, request, *args, **kwargs):
# print(1/0)
response = super().list(request, *args, **kwargs)
results = response.data['results']
data_list = []
for item in results:
# item为一条项目数据所在的字典
# 需要获取当前项目所属的接口总数、用例总数、配置总数、套件总数
project_id = item.get('id')
# interface_count = Interfaces.objects.filter(project_id=project_id).count()
# interface_qs = Interfaces.objects.filter(project_id=project_id)
# for obj in interface_qs:
# interface_id = obj.id
# TestCase.ojbects.filter(interface_id=interface_id).count()
# a.使用.annotate()方法,那么会自动使用当前模型类的主键作为分组条件
# b.使用.annotate()方法里可以添加聚合函数,计算的名称为一般从表模型类名小写(可以在外键字段上设置related_name)
# c.values可以指定需要查询的字段(默认为所用字段)
# d.可以给聚合函数指定别名,默认为testcases__count
# e.如果values放在annotate前面,那么聚合运算的字段不需要在values中添加,放在后面需要
# interfaces_obj = Interfaces.objects.annotate(testcases1=Count('testcases')).values('id', 'testcases1').\
# filter(project_id=project_id)
interface_testcase_qs = Interfaces.objects.values('id').annotate(testcases=Count('testcases')). \
filter(project_id=project_id)
# 获取项目下的接口总数
interfaces_count = interface_testcase_qs.count()
# 定义初始用例总数为0
testcases_count = 0
for one_dict in interface_testcase_qs:
testcases_count += one_dict.get('testcases')
# 获取项目下的配置总数
interface_configure_qs = Interfaces.objects.values('id').annotate(configures=Count('configures')). \
filter(project_id=project_id)
configures_count = 0
for one_dict in interface_configure_qs:
configures_count += one_dict.get('configures')
# 获取项目下套件总数
testsuites_count = Testsuits.objects.filter(project_id=project_id).count()
item['interfaces'] = interfaces_count
item['testcases'] = testcases_count
item['testsuits'] = testsuites_count
item['configures'] = configures_count
data_list.append(item)
response.data['results'] = data_list
return response
@action(methods=['get'], detail=False)
def names(self, request, *args, **kwargs):
# return self.list(request, *args, **kwargs)
qs = self.get_queryset()
serializer = self.get_serializer(qs, many=True)
return Response(serializer.data)
@action(detail=True)
def interfaces(self, request, *args, **kwargs):
# instance = self.get_object()
# # qs = Interfaces.objects.filter(projects=instance)
# serializer_obj = self.get_serializer(instance=instance)
# # 进行过滤和分页操作
# return Response(serializer_obj.data)
# return self.retrieve(request, *args, **kwargs)
response = self.retrieve(request, *args, **kwargs)
response.data = response.data['interfaces']
return response
@action(methods=['post'], detail=True)
def run(self, request, *args, **kwargs):
# 取出并构造参数
instance = self.get_object()
response = super().create(request, *args, **kwargs)
env_id = response.data.serializer.validated_data.get('env_id')
testcase_dir_path = os.path.join(settings.SUITES_DIR, datetime.strftime(datetime.now(), '%Y%m%d%H%M%S%f'))
# 创建一个以时间戳命名的路径
os.mkdir(testcase_dir_path)
env = Envs.objects.filter(id=env_id).first()
interface_qs = Interfaces.objects.filter(project=instance)
if not interface_qs.exists():
data = {
'ret': False,
'msg': '此项目下无接口,无法运行'
}
return Response(data, status=400)
runnable_testcase_obj = []
for interface_obj in interface_qs:
# 当前接口项目的用例所在查询集对象
testcase_qs = Testcases.objects.filter(interface=interface_obj)
if testcase_qs.exists():
# 将两个列表合并
runnable_testcase_obj.extend(list(testcase_qs))
if len(runnable_testcase_obj) == 0:
data = {
'ret': False,
'msg': '此项目下无用例,无法运行'
}
return Response(data, status=400)
for testcase_obj in runnable_testcase_obj:
# 生成yaml用例文件
common.generate_testcase_file(testcase_obj, env, testcase_dir_path)
# 运行用例(生成报告)
# common.run_testcase(instance, testcase_dir_path)
return common.run_testcase(instance, testcase_dir_path)
def get_serializer_class(self):
if self.action == 'names':
return ProjectsNamesModelSerializer
elif self.action == 'interfaces':
return InterfacesByProjectIdModelSerializer
elif self.action == 'run':
return ProjectsRunSerializer
# return InterfacesByProjectIdModelSerializer1
else:
return self.serializer_class
def perform_create(self, serializer):
if self.action == 'run':
pass
else:
serializer.save()
需要安装django-filter==2.3.0,然后在settings.py中注册应用django_filters。
设计子路由
from django.urls import path, re_path
from rest_framework.routers import DefaultRouter, SimpleRouter
# from projects.views import
from projects import views
# 定义路由对象
# router = SimpleRouter()
# DefaultRouter相比SimpleRouter,自动添加了一条根路径的路由 / -> 可浏览器的api页面
router = DefaultRouter()
# 使用路由对象.register()方法,来进行注册
# a.第一个参数指定路由前缀,r'子应用名小写'
# b.第二个参数指定视图集类即可,不要调用.as_view()
router.register(r'projects', views.ProjectsViewSet)
urlpatterns = []
# 使用路由对象.urls属性来获取自动生成的路由条目,往往为列表
# 需要将这个列表添加至urlpatterns
urlpatterns += router.urls
设计好之后,将子路由添加到主路由里。
运行测试
如上,已经提示要进行认证才能请求了。
我们进入接口文档里测试一下:http://127.0.0.1:8000/docs/
登陆之后拿到token。
添加token
请求一个需要验证的接口:
如图,通过添加token后,请求成功。