接口自动化测试分层设计与实践总结01

news2024/11/24 9:50:57

本文以笔者当前使用的自动化测试项目为例,浅谈分层设计的思路,不涉及到具体的代码细节和某个框架的实现原理,重点关注在分层前后的使用对比,可能会以一些伪代码为例来说明举例。

接口测试三要素

  • 参数构造

  • 发起请求,获取响应

  • 校验结果

一、原始状态

当我们的用例没有进行分层设计的时候,只能算是一个“苗条式”的脚本。以一个后台创建商品活动的场景为例,大概流程是这样的(默认已经是登录状态下):

创建商品-创建分类-创建优惠券-创建活动

要进行接口测试的话,按照接口测试的三要素来进行,具体的效果如下:

 # 1、参数构造
    createCommodityParams = {
        "input": {
            "title": "活动商品",
            "subtitle": "",
            "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": "颜色",
                            "attrValue": "绿色",
                            "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.randint(1,10)
    createCategoryParams["input"]["categoryName"] = "autoTestCategory" + str(time.time())
    createCouponParams。。。
    createPublicityParams。。。
    publishCommodityParams。。。
    publishPublicityParams。。。
   
    # 2、发起请求,获取响应
     # 创建商品并获取商品code
    createCommodityRes = api.getUrl("testApi.create.commodity").post.params(createCommodityParams)
    commodityCode = createCommodityRes["commodityCode"]
     # 创建分类并获取分类code
    createCategoryRes = api.getUrl("testApi.create.category").post.params(createCategoryParams)
    categoryCode = createCategoryRes["categoryCode"]
     # 创建优惠券并获取优惠券code
    createCouponRes = api.getUrl("testApi.create.coupon").post.params(createCouponParams)
    couponCode = createCouponRes["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"])
    。。。

按照上面的写法,对于单个脚本的调式来说或许可以,但是一旦用例的数量和复杂程度积累起来后,其维护成本将是巨大的,或者可以说不具备可维护性。

弊端说明

  • 可读性差,所有的处理都放在一起,代码量大,不简洁直观

  • 灵活性差,参数写死在脚本,适用用例范围小

  • 复用性差,如果其他用例需要同样或类似的步骤,需要重新写一份

  • 维护性差,如果接口有任何改动,那么所有涉及到此接口的脚本都需要一一修改

例如:随着用例场景的增加,就可能会出现下面这种情况

image|800x351

按照原始的模式,我们就需要些3个脚本文件分别来描述着3个场景,并且创建商品_API创建分类_API创建优惠券_API在场景1,2,3中均出现了;上架商品_API在场景2,3中均出现。由此我们完全可以预见到,当几百上千的用例场景出现后,这种形式是没有维护性可言的。

二、进化历程

因此我们依照着痛点,以最开始的原始状态为例,对用例进行分层改造,来看看进化后的状态。

1、API 定义层

我们编程的时候会将一些重复的代码进行封装使用,那么这里依然可以借用这种思想,我们将 API 的定义单独抽离,单独定义。

我们期望的效果是这样的:

image|800x562

提前将API的定义放在一层,供用例场景引用,这样当接口有任何修改时,我们只需要修改API definition层即可。

实例演示

对应着上面的demo,我们就是需要做如下抽离:

class APIDefinition: ''' 创建商品API定义 createCommodityParams: 创建商品接口入参 return:创建商品接口响应结果 ''' def createCommodityRequest(createCommodityParams): return api.getUrl("testApi.create.commodity").post.params(createCommodityParams)

     '''
     创建分类API定义
     createCategoryParams: 创建分类接口入参
     return:创建分类接口响应结果
     ''' 
     def createCategoryRequest(createCategoryParams)
      return api.getUrl("testApi.create.category").post.params(createCategoryParams)
     
     # 创建优惠券接口定义
     def createCouponRequest(createCouponParams)
      return api.getUrl("testApi.create.coupon").post.params(createCouponParams)
    
     # 创建活动接口定义
     def createPublicityRequest(createPublicityParams)
      return api.getUrl("testApi.create.publicity").post.params(createPublicityParams)
    
     # ...其余省略
2、Service 层

上面我们已经将接口的定义抽离出来,解决了 API 重复定义的问题,但是再继续分析会发现有一个问题依然没有解决,就是场景的复用性.

再看刚才的图:

image|800x562

3个场景中都有重复的步骤,类似创建商品创建分类创建优惠券这些,并且这些步骤都是一个个API的组合,一个步骤对应一个API,在各个步骤之间还会有数据的处理与传递,为了解决这些问题,将对场景再次做抽离,这里我称之为 service 层。

这一层之所以叫做service(服务)层,是因为它的作用是用来提供测试用例所需要的各种“服务”,好比参数构建、接口请求、数据处理、测试步骤。

用下图先来看分层的目标:

image|800x866

我们希望将常用的测试场景步骤封装至service层中,供用例场景调用,增加复用性,也可以理解为测试用例的前置处理;

但是这里还是有一点小问题,就是service层的东西太多太杂,有些场景步骤可能只适用于我当前的项目用例,在实际的工作中,各个系统间是相互依赖的,前台APP的测试很大可能就依赖后台创建作为前置条件

好比我在APP端只要商品和分类,可能只想创建商品和分类,并不想创建优惠券,这个时候service层就没有适用的场景步骤供调用,那么我就需要根据自己的需要重新封装;可是对于很多单接口的前置数据处理又是一致的,比如:

     createCommodityParams["input"]["title"] = "autoTest" + str(time.time())
        createCommodityParams["input"]["mallCode"] = self.mallCode
        createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
        createCategoryParams["input"]["categoryName"] = "autoTestCategory" + str(time.time())
        createCouponParams。。。
        createPublicityParams。。。
        publishCommodityParams。。。
        publishPublicityParams。。。

重新封装的话还要再处理这一步,就有点麻烦且不符合我们的复用性设计了,因此我们对service层再细化为3层,分别为:

apiObject

单接口的预处理层,这一层主要作用是单接口入参的构造,接口的请求与响应值返回

  • 每个接口请求不依赖与业务步骤,都是单接口的请求;

  • 此外一些简单固定的入参构建也直接放在这里处理,比如随机的商品名,title等,和具体业务流程无关,针对所有调用此接口的场景均适用

caseService

多接口的预处理层,这一层主要是测试步骤(teststep)或场景的有序集合。

  • 用例所需要的步骤,通过每一个请求进行组合,每一个步骤都对应着一个API请求,这些步骤会组成一个个场景,各个场景之间可以互相调用组成新的场景,以适应不同的测试用例需求。

  • 场景封装好以后可以供不同的测试用例调用,除了当前项目的用例,其他业务线需要的话也可从此caseService中选择调用,提高复用性的同时也避免了用例相互依赖的问题。

util

这一层主要放置针对当前业务的接口需要处理的数据

  • 在实际编写测试步骤时,可能部分接口的参数是通过其他接口获取后经过处理才可以使用,或是修改数据格式,或是修改字段名称,亦或是某些 value 的加解密处理等。

细化分层后,各层的职责便更加清晰明确,具体如下图:

image|800x855

实例演示

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"] = self.mallCode
          createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
          return createCommodityParams
        
         def createCategoryParamsBuild(createCategoryParams):
          ...
        
         def createCouponParamsBuild(createCouponParams):
          ...
        
         ......

到此,我们来看看原始的用例经过目前封装后的模样:

1、参数构造

    createCommodityParams = {
        "input": {
            "title": "活动商品",
            "subtitle": "",
            "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": "颜色",
                            "attrValue": "绿色",
                            "attrValueId": "1001"
                        }
                    ]
                }
            ],
            "jumpSwitch":false,
            "recommendCommodityCodeList": [],
            "recommendFittingCodeList": [],
            "mallCode": "8h4xxx"
        }
    }
    createCategoryParams = {......}
    createCouponParams = {......}
    createPublicityParams = {......}
    publishCommodityParams = {......}
    publishPublicityParams = {......}
    
    # 2、发起请求,获取响应
     # 创建商品并获取商品code
    createCommodityRes = ApiObject().createCommodity(createCommodityParams)
    commodityCode = createCommodityRes["commodityCode"]
     # 创建分类并获取分类code
    createCategoryRes = ApiObject().createCategory(createCategoryParams)
    categoryCode = createCategoryRes["categoryCode"]
     # 创建优惠券并获取优惠券code
    createCouponRes = ApiObject().createCoupon(createCouponParams)
    couponCode = createCouponRes["couponCode"]
     # 创建活动并关联商品,绑定优惠券,设置分类
    createPublicityParams["input"]["commodityCode"] = commodityCode
    createPublicityParams["input"]["categoryCode"] = categoryCode
    createPublicityParams["input"]["couponCode"] = couponCode
    createPublicityRes = ApiObject().createPublicity(createPublicityParams)
    
    # 结果校验(断言)
    assert.equal(createPublicityRes["code"], 0)
    assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
    。。。
  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"] = self.mallCode
          createCommodityParams["input"]["skuList"][0]["price"] = random.randint(1,10)
          return createCommodityParams
        
         def createCategoryParamsBuild(createCategoryParams):
          ...
        
         def createCouponParamsBuild(createCouponParams):
          ...
        
         ......

这时体现在用例中的表现就如下层testcase层所示.

3、testcase 层 我们想要的是一个清晰明了,“一劳永逸”的自动化测试用例,就像我们的手工测试用例一样,我们的前置条件可以复用,我们入参可以任意修改,但测试步骤都是固定不变的(前提可能是产品没有偷偷改需求~)。

这一层其实是对应的testsuite(测试用例集),是测试用例的无序集合。其中各个用例之间应该是相互独立,互不干扰,不存在依赖关系,每个用例都可以单独运行。

最终我们期望自动化用例的维护过程中达到的效果如下:

image|443x462

testcase 层:

可以看到,现在接口请求的url、method、通用入参处理等已经不会在用例中体现了,接下来继续封装caseService层。

caseService:

我们将多接口的场景步骤进行封装
```    class CaseService:
         def createPublicityByCategory(params):
           # 创建商品并获取商品code
          createCommodityRes = ApiObject().createCommodity(createCommodityParams)
          commodityCode = createCommodityRes["commodityCode"]
           # 创建分类并获取分类code
          createCategoryRes = ApiObject().createCategory(createCategoryParams)
          categoryCode = createCategoryRes["categoryCode"]
           # 创建优惠券并获取优惠券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
        
         ......

可以看到,这时涉及到用例场景步骤的代码已经非常少了,并且完全独立,与框架、其他用例等均无耦合。

到这里我们再看用例,会发现一点,测试数据依然冗长,那么下面就开始对测试数据进行参数化和数据驱动的处理。

4、testdata 此层用来管理测试数据,作为参数化场景的数据驱动。

参数化: 所谓参数化,简单来说就是将入参利用变量的形式传入,不要将参数写死,增加灵活性,好比搜索商品的接口,不同的关键字和搜索范围作为入参,就会得到不同的搜索结果。上面的例子中其实已经是参数化了。

数据驱动:对于参数,我们可以将其放入一个文件中,可以存放多个入参,形成一个参数列表的形式,然后从中读取参数传入接口即可。常见做数据驱动的有 JSON、CSV、YAML 等。

实例演示

我们以CSV为例,不特别依照某个框架,通常测试框架都具备参数化的功能。

将所需要的入参放入test.csv文件中:

  createCommodityParams,createCategoryParams,...
        {
             "input": {
                 "title": "活动商品",
                 "subtitle": "",
                 "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": "颜色",
                                 "attrValue": "绿色",
                                 "attrValueId": "1001"
                             }
                         ]
                     }
                 ],
                 "jumpSwitch":false,
                 "recommendCommodityCodeList": [],
                 "recommendFittingCodeList": [],
                 "mallCode": "8h4xxx"
             }
         },
         ...

createCommodityParams,createCategoryParams,... { "input": { "title": "活动商品", "subtitle": "", "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": "颜色", "attrValue": "绿色", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } }, ...

然后再回到用例层,利用框架参数化的功能对数据进行读取

    # 1、参数构造
        @parametrize(params = readCsv("test.csv"))
        # 2、发起请求,获取响应
        createPublicityRes = CaseService().createPublicityByCategory(params)
        # 结果校验(断言)
        assert.equal(createPublicityRes["code"], 0)
        assert.equal(createPublicityRes["publicityName"], createPublicityParams["publicityName"])
        。。。

注:这里的测试数据,不仅仅局限于接口的请求参数,既然做数据驱动,那么断言也可以维护在此,以减少用例层的代码冗余。

5、rawData 这一层是存放接口原始入参的地方。

某些接口的入参可能很多,其中很多参数值又可能是固定不变的,构建入参的时候我们只想对"变"的值进行动态的维护,而不维护的值就使用原始参数中的默认值,以此减少工作量(emmm…可能也就是CV大法的量吧~)

再者就是数据驱动的数据文件中只维护需要修改的参数,使数据文件更简洁,可阅读性更强。

实例演示:

这种利用原始参数(rawData)的方法我们称之为模板化,实际工作中有多种方式可实现,例如jsonpath、Mustache或者自己根据需求实现方法,本文重点在介绍分层设计,所以就不具体演示模板化技术的细节了,仅说明设计此层的作用。

以实例中的入参createCommodityParams为例,未用模板化技术前,我们要在CSV里面维护完整的入参:

 createCommodityParams,createCategoryParams,...
     {
          "input": {
              "title": "活动商品",
              "subtitle": "",
              "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": "颜色",
                              "attrValue": "绿色",
                              "attrValueId": "1001"
                          }
                      ]
                  }
              ],
              "jumpSwitch":false,
              "recommendCommodityCodeList": [],
              "recommendFittingCodeList": [],
              "mallCode": "8h4xxx"
          }
      },
      ...

createCommodityParams,createCategoryParams,... { "input": { "title": "活动商品", "subtitle": "", "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": "颜色", "attrValue": "绿色", "attrValueId": "1001" } ] } ], "jumpSwitch":false, "recommendCommodityCodeList": [], "recommendFittingCodeList": [], "mallCode": "8h4xxx" } }, ...

但是实际上,我们可能仅仅需要修改维护其中某个或某几个字段(例如只想维护商品价格),其余的使用默认值即可,使用模板化技术后可能在CSV中就是这样的表现:

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

或者这样

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

亦或使用Mustache,将需要修改的value进行参数化{{value}}。

我们可以看到,这样处理后的数据驱动的文件就变得简洁清晰的许多,当一个文件中维护了多个用例且入参字段很多时,这样维护起来就可以清晰的看出每个数据对应的用例的作用了;

price就是为了测试价格的,stock就是为了测试库存的,publish就是为了测试上下架的等等。

注: 当然,此层的使用视实际情况而定,有可能这个接口的参数本身就没多少,那么直接全量使用就行,或者你就是觉得数据量哪怕再大我都能分得清楚,看的明白,不用也rawData是可以的~

image|458x490

6、Base 此层主要放置我们需要处理的公共前置条件和一些自动化公共方法,也可以理解为公共的config和util。

在我们实际的自动化开发过程中,有很多前置条件或公共方法,比如登录处理,log 处理,断言方法或一些数据处理;

使用过程中所有的service和testcase层都会继承此类,这样这些公共方法和前置条件便可直接通用;在各个业务线之间也可保持一致性。

三、完结 最后,我们来看下整体分层后的目录结构总览:

 └─apiautotest
      └─project
       └─rawData(原始参数)
        ├─testRawData.json
       └─service(用例服务)
        └─apiObject(单接口预处理,单接口入参的构造,接口的请求与响应值返回)
         ├─testApiObject.py
              └─caseService(多接口预处理,测试步骤(teststep)或场景的有序集合)
               ├─testCaseService.py
              └─util(工具类)
               ├─util.py
          └─testcase(测试用例)
              └─testDataDriven(测试数据驱动)
               ├─testData.csv
              ├─testcase.py(测试用例集)
         └─testBase.py(测试基类,初始化和公共方法) 
      └─platformapi(Api定义)
       ├─testApiDefinition.py

最后感谢每一个认真阅读我文章的人,礼尚往来总是要有的,虽然不是什么很值钱的东西,如果你用得到的话可以直接拿走:

这些资料,对于【软件测试】的朋友来说应该是最全面最完整的备战仓库,这个仓库也陪伴上万个测试工程师们走过最艰难的路程,希望也能帮助到你! 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1178471.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Redis】SSM整合Redis注解式缓存的使用

🎉🎉欢迎来到我的CSDN主页!🎉🎉 🏅我是Java方文山,一个在CSDN分享笔记的博主。📚📚 🌟推荐给大家我的专栏《Redis》。🎯🎯 &#x1f4…

2000-2022年上市公司供应链数字化示范名单匹配数据

2000-2022年上市公司供应链数字化示范名单匹配数据 1、时间:2000-2022年 2、来源:商务部 3、指标: 上市公司供应链数字化(根据城市名单匹配):股票代码、年份、股票简称、中文全称、省份、城市、区县、上…

接口请求断言

接口请求断言是指在发起请求之后,对返回的响应内容去做判断,用来查看是否响应内容是否与规定的返回值相符。 在发起请求后,我们使用一个变量 r 存储响应的内容,也就是 Response 对象。 Response 对象有很多功能强大的方法可以调…

[动态规划] (九) 路径问题:LeetCode 64.最小路径和

[动态规划] (九) 路径问题:LeetCode 64.最小路径和 文章目录 [动态规划] (九) 路径问题:LeetCode 64.最小路径和题目解析解题思路状态表示状态转移方程初始化和填表顺序返回值 代码实现总结 64. 最小路径和 题目解析 (1) 从左上角到右下角 (2) 只能向右…

Poetry:Python开发者的依赖管理新时代

更多资料获取 📚 个人网站:涛哥聊Python 在Python开发中,管理项目的依赖关系是一个至关重要的任务。传统上,开发者使用requirements.txt文件和pip工具来管理依赖,但这种方式在复杂项目中存在一些问题。Poetry是一个现…

Docker 学习路线 5:在 Docker 中实现数据持久化

Docker 可以运行隔离的容器,包括应用程序和其依赖项,与主机操作系统分离。默认情况下,容器是临时的,这意味着容器中存储的任何数据在终止后都将丢失。为了解决这个问题并在容器生命周期内保留数据,Docker 提供了各种数…

kafka问题汇总

报错1: 解决方式 1、停止docker服务   输入如下命令停止docker服务 systemctl stop docker 或者service docker stop1   停止成功的话,再输入docker ps 就会提示出下边的话: Cannot connect to the Docker daemon. Is the docker daem…

通过全流量查看部门或客户端网络使用情况

近年来,随着数字化转型和云计算服务的广泛应用,组织和企业对于网络带宽和性能的需求也在不断增长。 网络的稳定性、性能和安全性对于业务流程的顺畅运行至关重要。因此,了解部门或客户端网络的使用情况是网络管理和优化的关键。本文将通过Ne…

【C++】STL容器适配器——stack类的使用指南(含代码使用)(17)

前言 大家好吖,欢迎来到 YY 滴C系列 ,热烈欢迎! 本章主要内容面向接触过C的老铁 主要内容含: 欢迎订阅 YY滴C专栏!更多干货持续更新!以下是传送门! 目录 一、stack 类——基本介绍二、stack 类…

if语句中的按位取反问题

🎀 文章作者:二土电子 🌸 关注公众号获取更多资源! 🐸 期待大家一起学习交流! 文章目录 一、现象描述1.1 在C语言中(非STM32)1.2 STM32中运行 二、基础知识复习2.1 原码、反码和补…

dell r720部署chatglm3,使用nvidia tesla P40+M40

dell r720的idrac的地址默认是192.168.1.110,root 默认密码calvin fatal Error! All channnels have been disabled due to all DIMMs failed the Memoey 是什么意思 Dell PowerEdge T320服务器 开机显示 Fatal Errort!all channells have been disabled due to …

NSSCTF第11页(1)

[HUBUCTF 2022 新生赛]Calculate 进到主页 翻译 回答以下数学问题20次;你有3秒钟的时间来解决每个问题; 为了保护服务器,你不能在1秒内回答一个问题 您已回答0个问题; 让我们3秒速算,没那个实力,提示说是写…

Kotlin基础数据类型和运算符

原文链接 Kotlin Types and Operators Kotlin是新一代的基于JVM的静态多范式编程语言,功能强大,语法简洁,前面已经做过Kotlin的基本的介绍,今天就来深入的学习一下它的数据类型和运算操作符。 数据类型 与大部分语言不同的是&am…

Vue多级路由的实现

对Vue路由的配置不了解的看这篇文章:Vue-router 路由的基本使用-CSDN博客 一、配置children属性 注意:子路径不能带 / { path : "/父路径", component : 父组件, children : [{ path : "子路径", component : 子组件 }] } 二、配…

读源码的七大步骤

前序 在如今信息这么发达的时代,如何让自己成长得最快,莫过于直接去“学习”别人已经写好的产品。但一提到读源码,很多人都会比较畏惧,认为读源码是高手才会做的事情。毕竟动不动几十万行代码的源码,谁看了不害怕呢&a…

软件开发必备神器!一文读懂10款热门看板工具推荐!

看板(Kanban)是一种流行的框架,用于实施敏捷和DevOps软件开发。它要求实时沟通每个人的能力,并全面透明地展示正在进行的工作。工作项目在看板上以可视化方式表示,使项目经理和所有团队成员可以随时查看每个工作的状态…

云尘靶场 --铁三域控

签到1-eval ls System Assert 铁三域控 域就像是我们一个机房,老师的电脑控制很多很多电脑,这个机房就像一个域,老师是管理员身份,可以控制所有学生的电脑 下文的141就像老师,192.168.60.100就像学生的电脑 123 用…

Linux学习笔记之四(文件IO、目录IO)

Linux 1、文件IO1.1、open and close1.2、read and write1.3、lseek1.4、综合练习 2、目录IO2.1、mkdir2.2、opendir, closedir, readdir2.3、综合练习 1、文件IO 1.1、open and close 使用以下代码查看以下open函数原型: man 2 open如图,open函数有两…

测试开发角色定位

📢专注于分享软件测试干货内容,欢迎点赞 👍 收藏 ⭐留言 📝 如有错误敬请指正!📢交流讨论:欢迎加入我们一起学习!📢资源分享:耗时200小时精选的「软件测试」资…

python opencv 实现对二值化后的某一像素值做修改和mask叠加

实现对二值化后的某一像素值做修改 使用OpenCV的findNonZero函数找到所有非零(也就是像素值为255)的像素,然后遍历这些像素并修改他们的值。示例代码: import cv2 import numpy as np # 加载并二值化图像 img cv2.imread(…