20230409-测试平台实战二

PPT

测试平台训练营二

源码地址

HogwartsTestPlatform202303: 测试平台项目,前端vue,后端flask

训练营目标

  • 项目初始化
  • api接口封装
  • 布局实现
  • 测试用例组件实现
  • 测试计划组件实现
  • 测试报告组件实现

项目初始化1-详情

image


项目初始化2-预设

  • 进入预设界面
  • 默认第一个是vue3,选择手动配置项目
  • 点击下一步

image


项目初始化3-功能

  • 进入功能界面
  • 只勾选babel和router,其他全部取消
  • 点击下一步

image


项目初始化4-配置

  • 进入配置界面
  • 切换vue2.x,默认是vue3.x
  • 点击创建项目

image


项目初始化5-插件

  • 点击左侧插件,进入插件页面
  • 右上角输入vuetify,搜索后选择第一个进行安装
  • 安装完成界面会展示该插件


项目初始化6-依赖

  • 点击左侧依赖,进入依赖页面
  • 右上角输入axios,搜索后选择第一个进行安装
  • 安装完成界面会展示该依赖


启动项目

  • 点击左侧任务,进入任务页面
  • 选择serve
  • 点击运行
  • 点击启动app,即可打开页面


默认代码改造

  • 目录结构

image


api封装-http.js

// 完成 http 请求的基本配置
// 导入 axios
import axios from 'axios'

// 创建 axios 实例
var instance = axios.create({
    // 请求头
    headers: {
        'Content-Type': 'application/json'
    },
    // 超时时间
    timeout: 2500,
    // 基础 url,接口服务地址
    // baseURL: 'http://39.102.48.202:6099/'
    baseURL: 'http://127.0.0.1:5001/'
})

export default instance

api封装-http.js

// 完成 http 请求的基本配置
// 导入 axios
import axios from 'axios'

// 创建 axios 实例
var instance = axios.create({
    // 请求头
    headers: {
        'Content-Type': 'application/json'
    },
    // 超时时间
    timeout: 2500,
    // 基础 url,接口服务地址
    // baseURL: 'http://39.102.48.202:6099/'
    baseURL: 'http://127.0.0.1:5001/'
})

export default instance

api封装-testcase.js

// 测试用例增删改查接口管理

// 导入已经配置好的 axios 实例
import axios from './http'

const testcase = {
    // 获取用例信息
    getTestcase(params) {
        return axios({
            method: "GET",
            url: "/testcase",
            // 如果是传递拼接在 url 中的参数,要使用 params
            params: params
        })
    },
    // 添加用例
    addTestcase(data) {
        return axios({
            method: "POST",
            url: "/testcase",
            // 如果是传递请求体,要使用 data
            data: data
        })
    },
    // 删除用例
    deleteTestcase(data) {
        return axios({
            method: "DELETE",
            url: "/testcase",
            data: data
        })
    },
    // 修改用例
    updateTestcase(data) {
        return axios({
            method: "PUT",
            url: "/testcase",
            data: data
        })
    }
}

// 导出
export default testcase

api封装-plan.js

// 导入已经配置好的 axios 实例
import axios from './http'

const plan = {
    // 获取测试计划
    getPlan(params) {
        return axios({
            method: "GET",
            url: "/plan",
            // 如果是传递拼接在 url 中的参数,要使用 params
            params: params
        })
    },
    // 添加测试计划
    addPlan(data) {
        return axios({
            method: "POST",
            url: "/plan",
            // 如果是传递请求体,要使用 data
            data: data
        })
    },
    // 删除测试计划
    deletePlan(data) {
        return axios({
            method: "DELETE",
            url: "/plan",
            data: data
        })
    },
}

// 导出
export default plan

api封装-build.js

// 导入已经配置好的 axios 实例
import axios from './http'

const build = {
    // 获取构建记录
    getBuild(params) {
        return axios({
            method: "GET",
            url: "/build",
            // 如果是传递拼接在 url 中的参数,要使用 params
            params: params
        })
    },
    // 获取构建记录
    addBuild(data) {
        return axios({
            method: "POST",
            url: "/build",
            // 如果是传递请求体,要使用 data
            data: data
        })
    },
    // 删除构建记录
    deleteBuild(data) {
        return axios({
            method: "DELETE",
            url: "/build",
            data: data
        })
    },
}

// 导出
export default build

api封装-api.js

// 所有接口的入口,相当于目录
import testcase from './testcase'
import plan from './plan'
import build from './build'

const api = {
    testcase,
    plan,
    build,
}

export default api

api封装-api.js

// 所有接口的入口,相当于目录
import testcase from './testcase'
import plan from './plan'
import build from './build'

const api = {
    testcase,
    plan,
    build,
}

export default api

api封装-api.js

// 所有接口的入口,相当于目录
import testcase from './testcase'
import plan from './plan'
import build from './build'

const api = {
    testcase,
    plan,
    build,
}

export default api

启动入口-main.js

import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify'
import api from './api/api'

Vue.prototype.$api = api
Vue.config.productionTip = false

new Vue({
  router,
  vuetify,
  render: h => h(App)
}).$mount('#app')


最外层组件-App.vue

<template>
  <router-view></router-view>
</template>

<script>

export default {
  name: 'App',

  data: () => ({
    //
  }),
};
</script>


布局组件-Index.vue

<template>
    <v-app id="inspire">
        <!-- 侧边栏 -->
        <v-navigation-drawer v-model="drawer" app>
            <!-- 左侧logo -->
            <v-list-item>
                <v-list-item-content>
                    <v-list-item-title class="text-h6">
                        测试平台
                    </v-list-item-title>
                    <v-list-item-subtitle>
                        霍格沃兹
                    </v-list-item-subtitle>
                </v-list-item-content>
            </v-list-item>

            <!-- 左侧导航栏 -->
            <v-list dense nav>
                <v-list-item v-for="item in items" :key="item.title" link :href="item.link">
                    <v-list-item-icon>
                        <v-icon>{{ item.icon }}</v-icon>
                    </v-list-item-icon>
                    <v-list-item-content>
                        <v-list-item-title>{{ item.title }}</v-list-item-title>
                    </v-list-item-content>
                </v-list-item>
            </v-list>
        </v-navigation-drawer>

        <!-- 顶部栏 -->
        <v-app-bar app>
            <v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
            <v-toolbar-title>霍格沃兹测试平台</v-toolbar-title>
        </v-app-bar>

        <!-- 主体内容 -->
        <v-main>
            <router-view></router-view>
        </v-main>
    </v-app>
</template>

<script>
export default {
    data: () => ({
        drawer: null,
        items: [
            { title: '测试用例', icon: 'mdi-view-dashboard', link: '#/index/test_case' },
            { title: '测试计划', icon: 'mdi-image', link: '#/index/test_plan' },
            { title: '测试报告', icon: 'mdi-help-box', link: '#/index/test_report' },
        ],
        right: null,
    }),
}
</script>

路由封装-index.js

import Vue from 'vue'
import VueRouter from 'vue-router'
import Index from '../views/Index.vue'
import TestCase from '../views/TestCase.vue'
import TestPlan from '../views/TestPlan.vue'
import TestReport from '../views/TestReport.vue'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    redirect: '/index/test_case'
  },
  {
    path: '/index',
    name: 'Index',
    component: Index,
    children: [
      {
        path: 'test_case',
        name: 'TestCase',
        component: TestCase
      },
      {
        path: 'test_plan',
        name: 'TestPlan',
        component: TestPlan
      },
      {
        path: 'test_report',
        name: 'TestReport',
        component: TestReport
      },
    ]
  }
]

const router = new VueRouter({
  routes
})

export default router


测试用例组件-TestCase.vue

<template>
    <div>
        <!-- 测试用例数据表格 -->
        <v-data-table :headers="headers" :items="desserts" sort-by="calories" class="elevation-1" v-model="selected"
            item-key="id" show-select>
            <!-- 顶部插槽 -->
            <template v-slot:top>
                <v-toolbar flat>
                    <v-toolbar-title>测试用例</v-toolbar-title>
                    <v-divider class="mx-4" inset vertical></v-divider>
                    <v-spacer></v-spacer>
                    <!-- 生成测试计划弹框 -->
                    <v-dialog v-model="dialogPlan" max-width="500px">
                        <template v-slot:activator="{ on, attrs }">
                            <v-btn color="green" dark class="mb-2" v-bind="attrs" v-on="on">
                                生成计划
                            </v-btn>
                        </template>
                        <v-card>
                            <v-card-title>
                                <span class="text-h5">生成计划</span>
                            </v-card-title>

                            <v-card-text>
                                <v-container>
                                    <v-row>
                                        <v-text-field v-model="planName" label="测试计划名称"></v-text-field>
                                    </v-row>
                                </v-container>
                            </v-card-text>

                            <v-card-actions>
                                <v-spacer></v-spacer>
                                <v-btn color="blue darken-1" text @click="savePlan">
                                    保存
                                </v-btn>
                            </v-card-actions>
                        </v-card>
                    </v-dialog>

                    <!-- 新增用例弹框 -->
                    <v-dialog v-model="dialog" max-width="500px">
                        <template v-slot:activator="{ on, attrs }">
                            <v-btn color="primary" dark class="mb-2" v-bind="attrs" v-on="on">
                                新增用例
                            </v-btn>
                        </template>
                        <v-card>
                            <v-card-title>
                                <span class="text-h5">{{ formTitle }}</span>
                            </v-card-title>

                            <v-card-text>
                                <v-container>
                                    <v-text-field v-if="editedItem.id" v-model="editedItem.id" label="用例id"
                                        disabled></v-text-field>
                                    <v-text-field v-model="editedItem.name" label="用例名称"></v-text-field>
                                    <v-textarea v-model="editedItem.step" label="用例步骤"></v-textarea>
                                    <v-text-field v-model="editedItem.method" label="用例方法"></v-text-field>
                                    <v-text-field v-model="editedItem.remark" label="备注"></v-text-field>
                                </v-container>
                            </v-card-text>

                            <v-card-actions>
                                <v-spacer></v-spacer>
                                <v-btn color="blue darken-1" text @click="close">
                                    取消
                                </v-btn>
                                <v-btn color="blue darken-1" text @click="save">
                                    保存
                                </v-btn>
                            </v-card-actions>
                        </v-card>
                    </v-dialog>

                    <!-- 删除用例弹框 -->
                    <v-dialog v-model="dialogDelete" max-width="500px">
                        <v-card>
                            <v-card-title class="text-h5">确认删除用例?</v-card-title>
                            <v-card-actions>
                                <v-spacer></v-spacer>
                                <v-btn color="blue darken-1" text @click="closeDelete">取消</v-btn>
                                <v-btn color="blue darken-1" text @click="deleteItemConfirm">确认</v-btn>
                                <v-spacer></v-spacer>
                            </v-card-actions>
                        </v-card>
                    </v-dialog>
                </v-toolbar>
            </template>
            <!-- 表格内容操作插槽 -->
            <template v-slot:[`item.actions`]="{ item }">
                <v-icon small class="mr-2" @click="editItem(item)">
                    mdi-pencil
                </v-icon>
                <v-icon small @click="deleteItem(item)">
                    mdi-delete
                </v-icon>
            </template>
        </v-data-table>
    </div>
</template>

<script>
export default {
    data: () => ({
        dialog: false,
        dialogDelete: false,
        dialogPlan: false,
        planName: '',
        selected: [],
        headers: [
            { text: '用例ID', align: 'start', sortable: false, value: 'id', },
            { text: '用例名称', value: 'name' },
            { text: '用例步骤', value: 'step' },
            { text: '用例方法', value: 'method' },
            { text: '备注', value: 'remark' },
            { text: '操作', value: 'actions', sortable: false },
        ],
        desserts: [],
        editedIndex: -1,
        editedItem: {
            id: '',
            name: '',
            step: '',
            method: '',
            remark: ''
        },
        defaultItem: {
            id: '',
            name: '',
            step: '',
            method: '',
            remark: ''
        },
    }),

    computed: {
        formTitle() {
            return this.editedIndex === -1 ? '新增用例' : '编辑用例'
        },
    },

    watch: {
        dialog(val) {
            val || this.close()
        },
        dialogDelete(val) {
            val || this.closeDelete()
        },
    },

    created() {
        this.initData()
    },

    methods: {
        initData() {
            this.$api.testcase.getTestcase().then((result) => {
                console.log("getTestcase", result)
                // 渲染测试用例表体的数据
                this.desserts = result.data.data
            }).catch((err) => {
                console.log(err)
            });
        },

        editItem(item) {
            this.editedIndex = this.desserts.indexOf(item)
            this.editedItem = Object.assign({}, item)
            this.dialog = true
        },

        deleteItem(item) {
            this.editedIndex = this.desserts.indexOf(item)
            this.editedItem = Object.assign({}, item)
            this.dialogDelete = true
        },

        deleteItemConfirm() {
            // this.desserts.splice(this.editedIndex, 1)
            // 获取要删除用例的 id
            console.log(this.editedItem.id)
            // 调用删除接口
            this.$api.testcase.deleteTestcase({ 'id': this.editedItem.id }).then((result) => {
                if (result.data.code === 0) {
                    this.initData()
                } else {
                    console.log("用例删除失败")
                    console.log("deleteTestcase", result)
                }
            }).catch((err) => {
                console.log(err)
            });
            this.closeDelete()
        },

        close() {
            this.dialog = false
            this.$nextTick(() => {
                this.editedItem = Object.assign({}, this.defaultItem)
                this.editedIndex = -1
            })
        },

        closeDelete() {
            this.dialogDelete = false
            this.$nextTick(() => {
                this.editedItem = Object.assign({}, this.defaultItem)
                this.editedIndex = -1
            })
        },

        save() {
            if (this.editedIndex > -1) {
                // 编辑用例,调用更新用例的接口
                console.log("this.editedIndex > -1")
                // Object.assign(this.desserts[this.editedIndex], this.editedItem)
                console.log("this.editedItem", this.editedItem)
                this.$api.testcase.updateTestcase(this.editedItem).then((result) => {
                    if (result.data.code === 0) {
                        this.initData()
                    } else {
                        console.log("用例更新失败")
                        console.log("updateTestcase", result)
                    }
                }).catch((err) => {
                    console.logO(err)
                });
            } else {
                // 新增用例,调用新增用例的接口
                console.log("else")
                // this.desserts.push(this.editedItem)
                const { id, ...newEditedItem } = this.editedItem
                this.$api.testcase.addTestcase(newEditedItem).then((result) => {
                    if (result.data.code === 0) {
                        this.initData()
                    } else {
                        console.log("用例新增失败")
                        console.log("addTestcase", result)
                    }
                }).catch((err) => {
                    console.log(err)
                });
            }
            this.close()
        },

        // 生成测试计划的方法
        savePlan() {
            // 测试计划名称
            console.log("测试计划名称", this.planName)
            // 测试用例信息
            console.log("测试用例信息", this.selected)
            // this.selected 获取到的是一个对象,转换成数组
            let idList = [];
            this.selected.forEach(item => {
                console.log("获取到的id为", item.id)
                idList.push(item.id)
            })
            // 测试用例信息
            console.log("测试用例数组信息", idList)
            this.$api.plan.addPlan({ "name": this.planName, "testcase_ids": idList })
                .then((result) => {
                    console.log("添加测试计划是否成功", result)
                }).catch((err) => {
                    console.log(err)
                });
            // 执行完成初始选中状态并关闭弹窗
            this.planName = ''
            this.selected = []
            this.dialogPlan = false
        }
    },
}
</script>

测试用例组件-效果


测试计划组件-TestPlan.vue

<template>
    <div>
        <!-- 测试任务数据表格 -->
        <v-data-table :headers="headers" :items="desserts" :expanded.sync="expanded" item-key="name" class="elevation-1"
            show-expand>
            <template v-slot:top>
                <v-toolbar flat>
                    <v-toolbar-title>测试任务</v-toolbar-title>
                    <v-divider class="mx-4" inset vertical></v-divider>
                    <v-spacer></v-spacer>
                </v-toolbar>
            </template>
            <!-- 测试用例插槽 -->
            <template v-slot:[`item.testcases`]="{ item }">
                <span v-for="i in item.testcases">{{ i.name }},</span>
            </template>
            <!-- 展开行插槽 -->
            <template v-slot:expanded-item="{ headers, item }">
                <td :colspan="headers.length">
                    <h3 v-for="i in item.testcases">{{ JSON.stringify(i) }}</h3>
                </td>
            </template>
            <!-- 操作插槽 -->
            <template v-slot:[`item.actions`]="{ item }">
                <v-btn color="success" @click="buildPlan(item)">
                    执行
                </v-btn>
                <v-btn color="warning" @click="deletePlan(item)">
                    删除
                </v-btn>
            </template>
        </v-data-table>
    </div>
</template>
<script>
export default {
    data: () => ({
        expanded: [],
        dialog: false,
        headers: [
            { text: '计划ID', align: 'start', sortable: false, value: 'id' },
            { text: '计划名称', value: 'name' },
            { text: '关联用例', value: 'testcases' },
            { text: '操作', value: 'actions', sortable: false },
            { text: '展开查看', value: 'data-table-expand' },
        ],
        desserts: [],

    }),

    created() {
        this.initData()
    },

    methods: {
        // 初始化数据
        initData() {
            this.$api.plan.getPlan().then((result) => {
                console.log("获取到的测试计划为", result)
                this.desserts = result.data.data
            }).catch((err) => {
                console.log(err)
            })
        },
        // 执行计划
        buildPlan(item) {
            this.$api.build.addBuild({ 'id': item.id }).then((result) => {
                console.log("执行计划", result)
            }).catch((err) => {
                console.log(err)
            })
        },
        // 删除计划
        deletePlan(item) {
            this.$api.plan.deletePlan({ 'id': item.id }).then((result) => {
                console.log("删除计划", result)
                if (result.data.code === 0) {
                    this.initData()
                } else {
                    console.log("删除计划失败")
                    console.log("addTestcase", result)
                }
            }).catch((err) => {
                console.log(err)
            })
        }
    },
}
</script>

测试计划组件-效果


测试报告组件-TestReport.vue

<template>
    <div>
        <!-- 测试任务数据表格 -->
        <v-data-table :headers="headers" :items="desserts" sort-by="calories" class="elevation-1">
            <template v-slot:top>
                <v-toolbar flat>
                    <v-toolbar-title>测试报告</v-toolbar-title>
                    <v-divider class="mx-4" inset vertical></v-divider>
                    <v-spacer></v-spacer>
                </v-toolbar>
            </template>
            <!-- 计划插槽 -->
            <template v-slot:[`item.plan`]="{ item }">
                <v-tooltip bottom>
                    <template v-slot:activator="{ on, attrs }">
                        <v-btn color="primary" dark v-bind="attrs" v-on="on">
                            查看详情
                        </v-btn>
                    </template>
                    <h3> 计划id:{{ item.plan.id }}</h3>
                    <h3> 计划名称:{{ item.plan.id }}</h3>
                    <h3> 用例详情:</h3>
                    <h3 v-for="i in item.plan.testcases">{{ JSON.stringify(i) }}</h3>
                </v-tooltip>
            </template>
            <!-- 报告插槽 -->
            <template v-slot:[`item.report`]="{ item }">
                <a :href="item.report" target="_blank">{{ item.report }}</a>
            </template>
            <!-- 操作插槽 -->
            <template v-slot:[`item.actions`]="{ item }">
                <v-btn color="warning" @click="deleteBuild(item)">
                    删除
                </v-btn>
            </template>
        </v-data-table>
    </div>
</template>
<script>
export default {
    data: () => ({
        dialog: false,
        headers: [
            { text: '构建ID', align: 'start', sortable: false, value: 'id' },
            { text: '计划ID', value: 'plan_id' },
            { text: '计划详情', value: 'plan' },
            { text: '报告地址', value: 'report' },
            { text: '创建时间', value: 'create_time' },
            { text: '操作', value: 'actions', sortable: false },
        ],
        desserts: [],
    }),

    created() {
        this.initData()
    },

    methods: {
        initData() {
            this.$api.build.getBuild().then((result) => {
                console.log("获取到的测试计划为", result)
                this.desserts = result.data.data
            }).catch((err) => {
                console.log(err)
            });
        },
        // 删除报告
        deleteBuild(item) {
            console.log("item", item)
            this.$api.build.deleteBuild({ 'id': item.id }).then((result) => {
                if (result.data.code === 0) {
                    this.initData()
                } else {
                    console.log("删除报告失败")
                    console.log("addTestcase", result)
                }
            }).catch((err) => {
                console.log(err)
            })
        }
    },
}
</script>

测试报告组件-效果