Actions v/s Functions?

本文讨论了在使用QTP实施自动化项目时,选择使用Actions还是Functions的问题。通过对比两种方法的特点,作者分享了自己的经验,并提出了使用Functions的优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

When implementing automation for real projects using QTP, one question that puzzles many framework designers is whether to use Actions or Functions?

Well in this article I will be discussing the challenges associated with the use of Actions and why I personally prefer using Functions over Actions. The article is only based on my professional experience and personal opinions about the subject. One may or may not agree with the same

Before we start it is important for us to understand difference between Action and Function.

An Action is a feature of QTP while Function is a VBScript feature. An Action can have associated DataTable, Object Repository, Input/Output Parameters and return values. Functions on the other hand can only have input/output parameters (using ByVal or ByRef) and return values.

Consider the details on an Automation project which needs to be implemented

  • No. of scripts = 1200
  • No. of Unique code components (Re-usable + Non re-usable) = 500
  • Shared object repository with all objects
  • All scripts to be stored in HP Quality Center


First we will assume that we use Actions to implement the above project

  • The 500 re-usable actions that need to be created can be stored using different approaches
  1. One re-usable script per Test: We can store each re-usable action in an individual script. The problem with this approach though would be that finally we would have around 500 (hosting re-usable actions) + 1200 QTP scripts in the suite
  2. Store all re-usable actions in a Single Test: We can store all 500 re-usable actions in a single script. The problem with this approach is the Test containing all actions would become huge in size and any small corruption in the same can lead to disasters as it would require re-linking of all actions used in script
  3. Store multiple re-usable actions in scripts base on category: Assuming avg. 10 re-usable actions per script, we would have 50 scripts supporting these re-usable actions. This is better than above 2 approaches
  • To create the test case we need to make calls to external re-usable actions. The path to the test needs to be provided in such a case. One can provide an absolute path of the test where the re-usable action resides. But this creates a huge problem when the scripts containing re-usable actions are moved to a different location. This problem can be sorted out by using relative paths of the Test instead of an absolute path.

Now consider the challenges we will face using the above action approach

  • While debugging a test we come to know that one of the Actions needs to be updated with some additional code. To do so we will have to close the current script and open the script containing the re-usable action. Update the code and save the script. Then re-open the script which we were debugging and re-run the script. If during re-run we realize that there are few more changes required then we would have to repeat the process. This make maintenance a painful task
  • When upgrading QC or moving actions from one location to another location there is a risk of QTP scripts giving error in missing actions in missing resources pain. This would require to open each script and fix the issue
  • Since we are using shared object repository, each local object repository would be empty. When we use a blank action (No Code + No Local OR) the size occupied by the Action is 197KB. This means for 500 actions we would have ~96MB of space wasted for no reason. This is a huge overhead when using QC as the scripts are downloaded from the QC server to the local machine

To summarize the key challenges we faces using Actions are

  • Painful maintenance as QTP doesn’t allow opening multiple tests at the same time
  • Missing actions in script when moving re-usable actions from one location to another or QC version is upgraded
  • Corruption of the script containing the re-usable action will require all the callee tests to update the call. This would require to open each callee script and fix the call
  • Each actions consume additional 197KB of space

Now if we move to functions over Actions we can easily do away with above challenges

  • Instead of action the code would be stored in Function. These functions would be present in the library files.
  • QTP allows opening multiple library files at the same time. This makes debugging and maintenance also easier
  • Functions will only take the space required for the script code and no additional overhead
  • Functions are more flexible when it comes to re-usability

To conclude, Functions over Actions has many advantages and should be preferred in implementing any keyword or data driven framework

<template> <q-page :key="$route.fullPath"> <!--表头--> <q-banner class="q-py-sm q-px-xs"> <q-breadcrumbs> <q-breadcrumbs-el v-for="item in breadcrumbs" :key="item" :label="item" icon="widgets" /> </q-breadcrumbs> </q-banner> <div class="row q-py-xs q-px-xs items-center"> <div class="col-2"> <q-select dense outlined v-model="scriptListContext.currentView" :options="scriptListContext.views" option-label="name" option-value="id" emit-value map-options @update:model-value="changedView" fit :label="t('Views')" /> </div> <div class="q-mx-sm"> <q-btn v-if="scriptListContext.currentView == scriptListContext.recordType?.id" color="brown-5" :label="t('Customise View')" @click="customiseView" /> <q-btn v-else color="brown-5" :label="t('Edit View')" @click="editView" /> </div> <div><actions-bar-component v-model:script-list-context="scriptListContext" :actives="scriptListContext.buttons" :data="null"></actions-bar-component></div> </div> <q-card flat class="q-pl-xs q-pb-xs q-br-sm q-pt-sm"> <q-card-actions class="bg-grey-3" align="left" vertical> <q-btn color="grey" flat dense size="xs" :icon="expanded ? 'keyboard_arrow_up' : 'keyboard_arrow_down'" @click="expanded = !expanded"></q-btn> </q-card-actions> <q-slide-transition> <div v-show="expanded"> <div class="q-gutter-xs q-pa-sm"> <q-btn color="primary" @click="handleSearch" :label="t('Search')" /> <q-btn color="primary" @click="resetSearch" :label="t('Reset')" /> </div> <q-card-section v-show="search.searchAvailableFilters.length > 0" class="q-gutter-x-md items-center example-break-row"> <div class="row items-start example-container q-gutter-xs"> <template v-for="availableFilter in search.searchAvailableFilters" :key="availableFilter.id"> <template v-if="availableFilter.field.fieldViewType == 0"> <q-checkbox v-model="queryParams[availableFilter.fieldCustomId]" :label="availableFilter.field.name" left-label></q-checkbox> </template> <template v-else-if="availableFilter.field.fieldViewType == 7"> <q-input v-model="queryParamsTemp[availableFilter.fieldCustomId]" :label="availableFilter.field.name" @update:model-value=" (val) => { updateFilter(availableFilter.field, val); } " dense ></q-input> </template> <template v-else-if="availableFilter.field.fieldViewType == 2"> <q-input filled v-model="queryParamsTemp[availableFilter.fieldCustomId + '_from']" :label="availableFilter.field.name" dense @update:model-value=" (val) => { updateFilterDate(availableFilter, 'from', val); } " > <template v-slot:append> <q-icon name="event" class="cursor-pointer"> <q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-date v-model="queryParamsTemp[availableFilter.fieldCustomId + '_from']" mask="YYYY-MM-DD" @update:model-value=" (val, reason, details) => { updateFilterDate(availableFilter, 'from', val, reason, details); } " > <div class="row items-center justify-end"> <q-btn v-close-popup label="Close" color="primary" flat></q-btn> </div> </q-date> </q-popup-proxy> </q-icon> </template> </q-input> <span>至</span> <q-input filled v-model="queryParamsTemp[availableFilter.fieldCustomId + '_to']" :label="availableFilter.field.name" dense @update:model-value=" (val) => { updateFilterDate(availableFilter, 'to', val); } " > <template v-slot:append> <q-icon name="event" class="cursor-pointer"> <q-popup-proxy cover transition-show="scale" transition-hide="scale"> <q-date v-model="queryParamsTemp[availableFilter.fieldCustomId + '_to']" mask="YYYY-MM-DD" @update:model-value=" (val, reason, details) => { updateFilterDate(availableFilter, 'to', val, reason, details); } " > <div class="row items-center justify-end"> <q-btn v-close-popup label="Close" color="primary" flat></q-btn> </div> </q-date> </q-popup-proxy> </q-icon> </template> </q-input> </template> <template v-else-if="availableFilter.field.fieldViewType == 13"> <q-select v-model="queryParams[availableFilter.fieldCustomId]" :options="fieldOptions[availableFilter.fieldCustomId]" :label="availableFilter.field.name" use-input :option-value="availableFilter.field.fieldListOrRecordTypeIsList ? 'value' : 'id'" option-label="name" emit-value map-options @filter=" (val, update, abort) => { filterFn(availableFilter.field, val, update, abort); } " @filter-abort="abortFilterFn" @update:model-value="updateModelValue" :loading="optionLoading" dense clearable ></q-select> </template> </template> </div> </q-card-section> </div> </q-slide-transition> <q-card-section class="q-pl-xs q-pr-md q-pb-xs"> <q-table class="my-sticky-header-last-column-table" row-key="id" separator="cell" :rows="scriptListContext.items" :columns="columns" dense v-model:pagination="scriptListContext.pagination" :rows-per-page-options="pageOptions" :loading="loading" @request="onRequest" > <template v-slot:top="props"> <q-checkbox v-model="showInactives" :label="t('ShowInactives')"></q-checkbox> <q-space></q-space> <q-btn color="primary" icon-right="archive" :label="t('ExportToExcel')" no-caps @click="exportTable"></q-btn> <q-btn flat round dense :icon="props.inFullscreen ? 'fullscreen_exit' : 'fullscreen'" @click="props.toggleFullscreen" class="q-ml-md"></q-btn> </template> <template v-slot:body="props"> <q-tr :props="props"> <q-td v-for="col in columns" :key="col.name" :props="props"> <div> {{ props.row.value }} <div v-if="col.name == 'index'">{{ props.rowIndex + 1 }}</div> <template v-else-if="col.name == 'actions'"> <actions-bar-component v-model:script-list-context="scriptListContext" :actives="scriptListContext.buttonsRow" :data="props.row"></actions-bar-component> </template> <q-checkbox v-else-if="col.fieldModel?.fieldViewType == fieldViewTypeEnum.CheckBox" v-model="props.row[col.fieldModel.customId]" dense disable></q-checkbox> <template v-else-if="col.fieldModel?.fieldViewType == fieldViewTypeEnum.ListOrRecord"> {{ props.row[col.name] }} </template> <template v-else> {{ props.row[col.name] }} </template> </div> </q-td> </q-tr> </template> </q-table> </q-card-section> </q-card> </q-page> </template> <script setup lang="ts"> import { fetchListResult } from "src/api/customization/search"; import ActionsBarComponent from "src/components/ViewContent/ActionsBarComponent.vue"; import useTableList from "src/composables/useTableList"; import { IActive } from "src/interfaces/IActive"; import { IField } from "src/interfaces/IField"; import { Iparams } from "src/interfaces/Iparams"; import { Icolumn } from "src/interfaces/Icolumn"; import { IScriptListContext } from "src/interfaces/IScriptListContext"; import { formateList } from "src/modules/common-functions/datetimeOpration"; import { exportExcel } from "src/modules/common-functions/excelOpration"; import { onMounted, ref, watch } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { toRecordTypePage } from "src/utils/routeRedirection"; import { fieldViewTypeEnum } from "src/enums/fieldViewTypeEnum"; import { operatorType } from "src/enums/operatorType"; import { getAction } from "src/api/manage"; import { addLoadingTotal, getLoadingTotal, loadingOne, setQuasar } from "src/modules/common-functions/loadingStatus"; import { listPage } from "src/modules/listPageCS"; const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const showInactives = ref(true); const breadcrumbs: string[] = String(route.name || "").split(","); const optionLoading = ref(false); const expanded = ref(true); const fieldOptions: Record<string, any> = ref({}); const queryParamsTemp: Record<string, any> = ref({}); //初始化查询参数 const queryParas = ref<Iparams>({ RecordTypeId: "", IsInActive: showInactives.value, SkipCount: 0, MaxResultCount: 1000, Filter: "", }); //行按钮 const defaultRowActives: Array<IActive> = [ { id: "btn-view", name: "view", label: "查看", displayAS: 0, function: "", showInView: false, showInEdit: false, location: "row", isStandard: true, }, { id: "btn-edit", name: "edit", label: "编辑", displayAS: 0, function: "", showInView: false, showInEdit: false, location: "row", isStandard: true, }, { id: "btn-delete", name: "delete", label: "删除", displayAS: 0, function: "", showInView: false, showInEdit: false, location: "row", isStandard: true, }, ]; //主表按钮 const defaultActives: Array<IActive> = [ { id: "btn-new", name: "new", label: "新建", displayAS: 0, function: "handleCreate", showInView: false, showInEdit: false, location: "main", isStandard: true, }, ]; //上下文对象 const scriptListContext = ref<IScriptListContext>({ items: [], recordType: { id: route.query.id as string }, fieldOptions: {}, title: "", views: [], currentView: "", fields: [], colsApi: "", rowsApi: "/master-currency/paged", pagination: { sortBy: "", descending: false, page: 1, rowsPerPage: 100, rowsNumber: 0, }, buttons: defaultActives, buttonsRow: defaultRowActives, addButton: (button: IActive) => addButton(button), removeButton: (buttonId: string) => removeButton(buttonId), }); //列属性 const columns = ref<Icolumn[]>([ { name: "index", label: "序号", field: "index", align: "center" as const, headerStyle: "width: 60px", sortable: false, }, { name: "curName", required: true, label: "币别名称", field: "curName", align: "left" as const, sortable: true, }, { name: "isoCode", align: "center" as const, label: "货币ISO代码", field: "isoCode", sortable: true, }, { name: "formatSymbol", label: "显示符号", field: "formatSymbol", sortable: true, }, { name: "isInActive", label: "禁用", field: "isInActive", sortable: true, }, { name: "actions", label: "操作", field: "actions", align: "center" as const, headerStyle: "width: 100px", sortable: false, }, ]); const _listPage = new listPage(scriptListContext as Ref<IScriptListContext>); // 加载数据 onMounted(async () => { await _listPage.pageInit(); getTableData(); }); watch( () => scriptListContext.value.items, (newValue, oldValue) => { formateList(newValue, columns.value); }, { deep: true } ); const addButton = function (button: IActive) { if (button.location?.toLowerCase() == "row") { scriptListContext.value.buttonsRow.push(button); } else { scriptListContext.value.buttons.push(button); } }; const removeButton = function (buttonId: string) { const rb = scriptListContext.value.buttonsRow.find((item: IActive) => item.id == buttonId); if (rb) { scriptListContext.value.buttonsRow.splice(scriptListContext.value.buttonsRow.indexOf(rb), 1); } const btn = scriptListContext.value.buttons.find((item: IActive) => item.id == buttonId); if (btn) { scriptListContext.value.buttons.splice(scriptListContext.value.buttons.indexOf(btn), 1); } }; const { $q, queryParams, pageOptions, loading, onRequest, //服务器端分页 search, getTableData, //初始化加载 handleSearch, //search按钮 resetSearch, //reset按钮 } = useTableList(scriptListContext as Ref<IScriptListContext>, t); setQuasar($q); /** ========== export excel ============== */ const exportTable = async () => { const exportData = await getExportData(); exportExcel(exportData, columns.value); }; const getExportData = async () => { const params: Iparams = { Sorting: scriptListContext.value.pagination.sortBy, Descending: scriptListContext.value.pagination.descending, SkipCount: 0, MaxResultCount: 300, RecordTypeId: "", IsInActive: false, Filter: "", }; const filterParams = { Filter: JSON.stringify(queryParams.value), }; if (!scriptListContext.value.pagination.rowsNumber) return []; const totalPage = scriptListContext.value.pagination.rowsNumber / params.MaxResultCount; addLoadingTotal(totalPage); let exportData: Array<any> = []; for (let i = 0; i < totalPage; i++) { const allParams = Object.assign({}, params, filterParams); await getAction(scriptListContext.value.rowsApi, allParams).then((res) => { exportData = exportData.concat(res.items); params.SkipCount += params.MaxResultCount; loadingOne(); }); } addLoadingTotal(-1 * getLoadingTotal()); return exportData; }; /** ============= 过滤条件 ================ */ const getFieldOptions = async (field: IField, query: Iparams) => { optionLoading.value = true; await fetchListResult(field.fieldListOrRecordTypeId, query) .then((response) => { fieldOptions.value[field.customId] = response.items; }) .catch((res) => { console.log("error res:", res); }) .finally(() => { optionLoading.value = false; }); }; const filterFn = async (field: IField, val: string, update: any, abort: any) => { const queryFilter: any | object = {}; queryFilter["keywords"] = `opt_${operatorType.Like} ${val}`; // 'opt_6 ' + val queryParas.value.SkipCount = 0; queryParas.value.MaxResultCount = 100; queryParas.value.Filter = JSON.stringify(queryFilter); if (!val) { update(async () => { await getFieldOptions(field, queryParas.value); }); return; } update(async () => { await getFieldOptions(field, queryParas.value); }); }; const abortFilterFn = () => { // console.log('delayed filter aborted') }; const updateModelValue = (val: any) => { // console.log('updateModelValue', val) }; const updateFilterDate = (availableFilter: any, direction: string, val: any, reason = "", details: object = {}) => { var values = [queryParamsTemp.value[availableFilter.fieldCustomId + "_from"], queryParamsTemp.value[availableFilter.fieldCustomId + "_to"]]; queryParams.value[availableFilter.fieldCustomId] = "opt_11" + " " + JSON.stringify(values); }; const updateFilter = (field: IField, val: any) => { queryParams.value[field.customId] = "opt_6" + " " + val; }; const changedView = (val: string) => { toRecordTypePage(router, scriptListContext.value.recordType?.customId as string, "list", "", val); }; /** ============= 视图定义 ================ */ const editView = () => { toRecordTypePage(router, "search", "edit", scriptListContext.value.currentView, "3a0eb999-3fa6-d262-4b4e-f85331d1ca7d", undefined, "_blank"); }; const customiseView = () => { toRecordTypePage(router, "search", "create", scriptListContext.value.currentView, "3a0eb999-3fa6-d262-4b4e-f85331d1ca7d", { copy: "T" }, "_blank"); }; </script> <style lang="sass"> .example-break-row .flex-break flex: 1 0 100% !important height: 0 !important .my-sticky-header-last-column-table /* height or max-height is important */ height: 70vh table border-bottom: 1px solid rgba(0, 0, 0, 0.12); /* specifying max-width so the example can highlight the sticky column on any browser window */ // max-width: 600px td:last-child /* bg color is important for td; just specify one */ background-color: #eeeeee tr th position: sticky /* higher than z-index for td below */ z-index: 2 /* bg color is important; just specify one */ background: #eeeeee /* this will be the loading indicator */ thead tr:last-child th /* height of all previous header rows */ top: 48px /* highest z-index */ z-index: 3 thead tr:first-child th top: 0 z-index: 1 tr:last-child th:last-child /* highest z-index */ z-index: 3 td:last-child z-index: 1 td:last-child, th:last-child position: sticky right: 0 /* prevent scrolling behind sticky top row on focus */ tbody /* height of all previous header rows */ scroll-margin-top: 48px tbody tr:nth-child(even) background-color:#fafafa a &:link, &:visited color: blue text-decoration: none &:hover color: purple &:active color: blue .text-orignblue color: red !important .horizontal-items display: flex flex-wrap: nowrap justify-content: space-between align-items: center > q-item margin-right: 10px &:last-child margin-right: 0 // 如果需要为 q-item 添加更多样式,可以在这里继续嵌套 // 例如: // &:hover // background-color: lightgray </style> 一句一句给我解释
07-31
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值