This article is a summary of the study time of @Yuegan, an outstanding student of Hogwarts Testing Academy.

In this paper, the author of the current use of the automation test project as an example, talking about the idea of hierarchical design, does not involve the specific code details and the implementation principle of a framework, focus on the use of comparison before and after the stratification, may take some pseudo-code as an example to illustrate the example.

Interface test three elements:

  • Parameters of structure

  • Initiate a request and get a response

  • Check the results

First, the original state

When our use case is not layered, it is a “slim” script. Take a scenario where a product activity is created in the background as an example. The process looks like this (by default, it is already logged in) :

Create an item – Create a category – Create a coupon – Create an activity

To carry out the interface test, according to the three elements of the interface test, the specific effects are as follows:

CreateCommodityParams = {"input": {"title": "categoryLevel1Code", "brand": "", "categoryLevel1Code": "12", "categoryLevel2Code": "1312", "categoryLevel3Code": "131211", "detail": [ { "uri": "ecommerce/1118d9.jpg", "type": 0 } ], "installInfo": { "installType": 1, "installFee": null }, "pictureList": [ { "uri": "ecommerce/222.jpg", "main": true } ], "postageInfo": { "postageType": 2, "postageFee": 1, "postageId": null }, "sellerDefinedCode": "", "publish": 1, "skuList": [ { "skuCode": "", "externalSkuCode": "", "price": 1, "retailPrice": 6, "stock": 100, "weight": 0, "suggestPrice": 0, "skuAttrValueList": [ { "attrCode": COLOR", "attrName": "COLOR", "attrValue": "green ", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } } createCategoryParams = {...... } createCouponParams = {...... } createPublicityParams = {...... } publishCommodityParams = {...... } publishPublicityParams = {...... } createCommodityParams["input"]["title"] = "autoTest" + str(time.time()) createCommodityParams["input"]["mallCode"] = Self. MallCode createCommodityParams [" input "] [" skuList "] [0] [" price "] = random. The randint (1, 10) CreateCategoryParams [" INPUT "]["categoryName"] = "autoTestCategory" + STR (time.time() createCouponParams ["input"]["categoryName"] = "categoryName" + STR (time.time()) createCouponParams... CreatePublicityParams... PublishCommodityParams... PublishPublicityParams... # 2 make a request Code createCommodityRes = api.geturl (" testapi.create.modity ").post.params(createCommodityParams) CommodityCode = createCommodityRes["commodityCode"] # create a category and get the category code createCategoryRes = api.getUrl("testApi.create.category").post.params(createCategoryParams) categoryCode = createCategoryRes["categoryCode"] # create coupon and get coupon code createCouponRes = api.geturl (" testapi.create.Coupon ").post.params(createCouponParams) couponCode = CreatePublicityParams ["input"]["commodityCode"] = commodityCode createPublicityParams["input"]["categoryCode"] = categoryCode createPublicityParams["input"]["couponCode"] = couponCode CreatePublicityRes = api.geturl (" testapi.create. Publicity ").post.params(createPublicityParams) # assert.equal(createPublicityRes["code"], 0) assert.equal(createPublicityRes["publicityName"], CreatePublicityParams [" publicityName "])...Copy the code

As written above, this may be fine for a single script modality, but once the number and complexity of use cases has accumulated, it can be costly to maintain, or not maintainable.

Disadvantages:

  • Poor readability, all processing together, large amount of code, not concise and intuitive

  • Poor flexibility, parameter writing dead in the script, applicable to a small range of use cases

  • Poor reusability and need to be rewritten if other use cases require the same or similar steps

  • Poor maintainability, if any interface changes, then all the scripts involved in this interface need to be modified

For example, as use case scenarios increase, the following situation may occur

According to the original pattern, we need 3 script files to describe 3 scenarios respectively, and create commodity _API, create category _API, and create coupon _API appear in scenarios 1, 2, and 3. The shelf item _API appears in scenario 2,3. It is entirely predictable that this form will not be maintainable when hundreds or thousands of use case scenarios appear.

Second, evolution

So we layered the use cases to look at the state after evolution, taking the original state as an example, according to the pain point.

1. API definition layer

When we’re programming, we’re going to encapsulate some of the repetitive code, so we can still use that idea here, we’re going to separate out the DEFINITION of the API and define it separately.

Here’s what we want:

Put the API definitions in a layer ahead of time for use case scenarios to reference, so that when there are any changes to the interface, we only need to modify the API Definition layer.

Examples demonstrate

Corresponding to the above demo, we just need to do the following extraction:

class APIDefinition:

CreateCommodityRequest (createCommodityParams) def createCommodityRequest(createCommodityParams) Return api.geturl (" testapi.create.modity ").post.params(createCommodityParams) "" createCategoryParams: Create a class interface entry parameter return: "Def createCategoryRequest(createCategoryParams) return Api.geturl (" testapi.create.category ").post.params(createCategoryParams) # create coupon interface def createCouponRequest(createCouponParams) return api.getUrl("testApi.create.coupon").post.params(createCouponParams) # Def createPublicityRequest(createPublicityParams) return api.getUrl("testApi.create.publicity").post.params(createPublicityParams) # ... The rest of the omittedCopy the code

2, the Service layer

Above we have removed the definition of the interface and solved the problem of repeated API definitions, but further analysis reveals that there is still one problem that has not been solved, which is the reuse of scenarios.

Look at that picture again:

Three scenarios have repeat steps, similar to create products, create categories, create a coupon, and the combination of these steps are each API, a step corresponding to an API, there will be data between the various steps of processing and transmission, in order to solve these problems, will do the scene again pull away, here is what I call the service layer.

This layer is called the Service layer because it is used to provide the various “services” required by test cases, such as parameter construction, interface requests, data processing, and test steps.

Look at the hierarchical goals below:

We want to encapsulate the commonly used test scenario steps into the Service layer for use case scenarios to increase reusability, which can also be understood as pre-processing of test cases.

However, there is still a small problem, that is, there are too many things in the Service layer and some scene steps may only be applicable to my current project use cases. In actual work, each system is interdependent, and the test of foreground APP may depend on background creation as a precondition

For example, if I only need commodities and categories on the APP side, I may only want to create commodities and categories, but do not want to create coupons. In this case, the Service layer has no applicable scene steps to call, so I need to repackage according to my own needs. However, the preprocessing of data for many single interfaces is consistent, such as:

createCommodityParams["input"]["title"] = "autoTest" + str(time.time()) createCommodityParams["input"]["mallCode"] = Self. MallCode createCommodityParams [" input "] [" skuList "] [0] [" price "] = random. The randint (1, 10) CreateCategoryParams [" INPUT "]["categoryName"] = "autoTestCategory" + STR (time.time() createCouponParams ["input"]["categoryName"] = "categoryName" + STR (time.time()) createCouponParams... CreatePublicityParams... PublishCommodityParams... PublishPublicityParams...Copy the code

Repackaging requires this step, which is a bit troublesome and does not conform to our reusability design. Therefore, we further refine the Service layer into 3 layers, which are respectively:

ApiObject:

Single interface preprocessing layer, this layer is mainly used to construct single interface input parameters, interface request and response values return

  • Each interface request is a single interface request, independent of any business step.

  • In addition, some simple fixed input constructs are handled directly here, such as random commodity names, titles, etc., independent of the specific business process, and applicable to all scenarios that invoke this interface

CaseService:

A preprocessing layer of multiple interfaces, which is primarily an ordered set of test steps or scenarios.

  • The steps required by a use case are combined with each request, and each step corresponds to an API request. These steps form a scenario, and each scenario can be called to form a new scenario to meet the requirements of different test cases.

  • After encapsulation, scenarios can be called by different test cases. In addition to the use cases of the current project, other lines of business can also choose to call from caseService, which improves reusability and avoids the problem of use case interdependence.

Util:

This layer is where the data needs to be processed by the interface for the current business

  • In the actual writing of the test steps, some parameters of the interface may be obtained through other interfaces and processed before they can be used, or the data format may be modified, or the field name may be modified, or the encryption and decryption of some values may be processed.

After the layers are refined, the responsibilities of each layer become clearer and clearer, as shown in the figure below:

Examples demonstrate

apiObject:

class ApiObject: def createCommodity(createCommodityParams): inputParams = ApiParamsBuild().createCommodityParamsBuild(createCommodityParams) response = APIDefinition().createCommodityRequest(inputParams) return response def createCategory(createCategoryParams): ... def createCoupon(createCouponParams): ... . class ApiParamsBuild: def createCommodityParamsBuild(createCommodityParams): createCommodityParams["input"]["title"] = "autoTest" + str(time.time()) createCommodityParams["input"]["mallCode"] = Sell.mallcode createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10) return createCommodityParams def createCategoryParamsBuild(createCategoryParams): ... def createCouponParamsBuild(createCouponParams): ... .Copy the code

At this point, let’s take a look at what the original use case looks like in its current package:

CreateCommodityParams = {"input": {"title": "categoryLevel1Code", "brand": "", "categoryLevel1Code": "12", "categoryLevel2Code": "1312", "categoryLevel3Code": "131211", "detail": [ { "uri": "ecommerce/1118d9.jpg", "type": 0 } ], "installInfo": { "installType": 1, "installFee": null }, "pictureList": [ { "uri": "ecommerce/222.jpg", "main": true } ], "postageInfo": { "postageType": 2, "postageFee": 1, "postageId": null }, "sellerDefinedCode": "", "publish": 1, "skuList": [ { "skuCode": "", "externalSkuCode": "", "price": 1, "retailPrice": 6, "stock": 100, "weight": 0, "suggestPrice": 0, "skuAttrValueList": [ { "attrCode": COLOR", "attrName": "COLOR", "attrValue": "green ", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } } createCategoryParams = {...... } createCouponParams = {...... } createPublicityParams = {...... } publishCommodityParams = {...... } publishPublicityParams = {...... } # 2. CreateCommodityRes = ApiObject(). CreateCommodity (createCommodityParams) commodityCode = CreateCategoryRes = ApiObject().createcategory (createCategoryParams) CategoryCode = createCategoryRes["categoryCode"] # Create coupon and get coupon code createCouponRes = ApiObject().createcoupon (createCouponParams) couponCode = createCouponRes["couponCode"] # createPublicityParams["input"]["commodityCode"] = commodityCode createPublicityParams["input"]["categoryCode"] = categoryCode createPublicityParams["input"]["couponCode"] = couponCode createPublicityRes = ApiObject().createPublicityParams # Assert. Equal (createPublicityRes["code"], 0) assert. Equal (createPublicityRes["publicityName"], createPublicityParams["publicityName"])...Copy the code

As you can see, the url, method, common input processing, etc. of the interface request are no longer reflected in the use case, so we continue to encapsulate the caseService layer.

caseService:

We encapsulate the multi-interface scenario steps

class CaseService: def createPublicityByCategory(params): CreateCommodityRes = ApiObject(). CreateCommodity (createCommodityParams) commodityCode = CreateCategoryRes = ApiObject().createcategory (createCategoryParams) CategoryCode = createCategoryRes["categoryCode"] # Create coupon and get coupon code createCouponRes = ApiObject().createcoupon (createCouponParams) couponCode = createCouponRes["couponCode"] # createPublicityParams["input"]["commodityCode"] = commodityCode createPublicityParams["input"]["categoryCode"] = categoryCode createPublicityParams["input"]["couponCode"] = couponCode createPublicityRes = ApiObject().createPublicity(createPublicityParams) return createPublicityRes ......Copy the code

The use case representation is then shown in the TestCase layer below.

3, testcase layer

What we want is a clear, “once and for all” automated test case, just like our manual test cases, where our preconditions can be reused, our inputs can be modified, but the test steps are fixed (if the product doesn’t change the requirements implicitly).

This layer is actually the corresponding TestSuite, an unordered collection of test cases. Among them, each use case should be independent of each other, do not interfere with each other, there is no dependence, each use case can run independently.

Ultimately, we expect the following results to be achieved during the maintenance of automated use cases:

The testcase layer:

CreateCommodityParams = {"input": {"title": "categoryLevel1Code", "brand": "", "categoryLevel1Code": "12", "categoryLevel2Code": "1312", "categoryLevel3Code": "131211", "detail": [ { "uri": "ecommerce/1118d9.jpg", "type": 0 } ], "installInfo": { "installType": 1, "installFee": null }, "pictureList": [ { "uri": "ecommerce/222.jpg", "main": true } ], "postageInfo": { "postageType": 2, "postageFee": 1, "postageId": null }, "sellerDefinedCode": "", "publish": 1, "skuList": [ { "skuCode": "", "externalSkuCode": "", "price": 1, "retailPrice": 6, "stock": 100, "weight": 0, "suggestPrice": 0, "skuAttrValueList": [ { "attrCode": COLOR", "attrName": "COLOR", "attrValue": "green ", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } } createCategoryParams = {...... } createCouponParams = {...... } createPublicityParams = {...... } publishCommodityParams = {...... } publishPublicityParams = {...... } # 2. Get the response createPublicityRes = CaseService().createPublicityByCategory(createCommodityParams,createCategoryParams,createCouponParams...) Equal (createPublicityRes["code"], 0) assert. Equal (createPublicityRes["publicityName"], 0) assert. CreatePublicityParams [" publicityName "])...Copy the code

As you can see, there is very little code at this point that involves the steps of the use case scenario, and it is completely independent, decoupled from the framework, other use cases, and so on.

Let’s look at the use cases here and see that the test data is still verbose, so let’s start parameterizing and data-driven processing of the test data.

4, testdata

This layer is used to manage test data as a data driver for parameterized scenarios.

Parameterization: The so-called parameterization simply means that the input parameter is passed in the form of variables, and the parameter is not written to death, so as to increase flexibility. For example, the interface of searching commodities, different keywords and search scope will be used as the input parameter, and different search results will be obtained. This is already parameterized in the example above.

Data-driven: For parameters, we can put them in a file, can hold multiple input parameters, form a parameter list form, and then read the parameters into the interface. Common data driven JSON, CSV, YAML and so on.

Examples demonstrate

Let’s take CSV as an example. Without a particular framework, test frameworks are usually parameterized.

Put the required input arguments into the test.csv file:

createCommodityParams,createCategoryParams,... {"input": {"title": "categoryLevel1Code", "brand": "", "categoryLevel1Code": "12", "categoryLevel2Code": "1312", "categoryLevel3Code": "131211", "detail": [ { "uri": "ecommerce/1118d9.jpg", "type": 0 } ], "installInfo": { "installType": 1, "installFee": null }, "pictureList": [ { "uri": "ecommerce/222.jpg", "main": true } ], "postageInfo": { "postageType": 2, "postageFee": 1, "postageId": null }, "sellerDefinedCode": "", "publish": 1, "skuList": [ { "skuCode": "", "externalSkuCode": "", "price": 1, "retailPrice": 6, "stock": 100, "weight": SuggestPrice: 0, "suggestPrice": 0, "skuAttrValueList": [{"attrCode": "COLOR", "attrName": "COLOR", "attrValue": "green ", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } }, ...Copy the code

Then go back to the use case layer and read the data using the parameterized function of the framework

Parametrize (params = readCsv("test.csv")) # parametrize(params = readCsv("test.csv") Get a response createPublicityRes = CaseService (.) createPublicityByCategory (params) # results verify (claim) assert.equal(createPublicityRes["code"], 0) assert.equal(createPublicityRes["publicityName"], CreatePublicityParams [" publicityName "])...Copy the code

Note: The test data here is not limited to the request parameters of the interface. Since it is data-driven, assertions can also be maintained here to reduce code redundancy in the use case layer.

5, rawData

This layer is where the raw input parameters of the interface are stored.

Some interfaces may have many input parameters, and many parameter values may be fixed. When we build the input parameters, we only want to maintain the “changed” values dynamically, and use the default values of the original parameters to reduce the workload (EMMM… Maybe it is the quantity of CV method ~)

Furthermore, only the parameters that need to be modified are maintained in data-driven data files, making the data files more concise and readable.

Example demonstration:

This method of using rawData is called templating, and it can be implemented in many ways, such as JsonPath, Mustache, or just implementing it on demand. This article focuses on hierarchical design, so I won’t show you the details of templating techniques, just the role of designing this layer.

Take createCommodityParams as an example. Before templating, we need to maintain the complete input parameter in CSV:

createCommodityParams,createCategoryParams,... {"input": {"title": "categoryLevel1Code", "brand": "", "categoryLevel1Code": "12", "categoryLevel2Code": "1312", "categoryLevel3Code": "131211", "detail": [ { "uri": "ecommerce/1118d9.jpg", "type": 0 } ], "installInfo": { "installType": 1, "installFee": null }, "pictureList": [ { "uri": "ecommerce/222.jpg", "main": true } ], "postageInfo": { "postageType": 2, "postageFee": 1, "postageId": null }, "sellerDefinedCode": "", "publish": 1, "skuList": [ { "skuCode": "", "externalSkuCode": "", "price": 1, "retailPrice": 6, "stock": 100, "weight": SuggestPrice: 0, "suggestPrice": 0, "skuAttrValueList": [{"attrCode": "COLOR", "attrName": "COLOR", "attrValue": "green ", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } }, ...Copy the code

In practice, however, we may only need to modify and maintain one or more of these fields (for example, only the price of goods) and use the default values for the rest, which might look like this in CSV with tempetization:

    createCommodityParams,createCategoryParams,...     {          "input": {              "skuList": [                  {                      "price": 1,                      "retailPrice": 6          }      },      ...
Copy the code

Or it

  - keyPath: $.input.skuList[0].price
    value: 1  - keyPath: $.input.skuList[0].retailPrice    value: 6
Copy the code

You can also use Mustache to parameterize the value that needs to be modified {{value}}.

We can see that the data-driven file after this processing becomes much simpler and clearer. When a file maintains multiple use cases and many entry fields, the maintenance can clearly see the role of each data corresponding to the use case.

Price is for testing prices, stock is for testing inventory, publish is for testing shelves, etc.

Note: Of course, the use of this layer depends on the actual situation, it may be that the interface parameters themselves are not many, so directly use the full line, or you just think that the amount of data, no matter how large, I can clearly distinguish, see clearly, do not use rawData is ok

6, the Base

This layer mainly houses the common preconditions we need to deal with and some of the automated common methods, also known as the common Config and util.

In our actual automated development process, there are many preconditions or common methods, such as login handling, log handling, assertion methods, or some data handling;

All service and TestCase layers in use inherit this class so that these public methods and preconditions are directly common; Consistency is also maintained across lines of business.

Three end,

Finally, let’s take a look at the overall hierarchical directory structure:

├─ ├─ garbage, ├─ garbage, ├─ garbage, ├─ garbage, ├─ garbage, ├─testApiObject. Py ├─ ├ eservice ├─ Exercises, ├─ exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises, exercises ├─ ├─ testCase.py (TestCase.py) ├─ TestBase.py (TestCase.py) ├─ TestBase.py (TestCase.py) ├─ TestBase.py (TestCase.py) ├─ TestBase.py (TestCase.py) ├─ TestBase.py (TestCase.py) ├─ TestBase.py (TestCase.pyCopy the code

Above, I look forward to discussing with you.

(Article from Hogwarts Testing Institute)