17
08

利用Locust测试销售出库

在第四章的HTTP协议部分,已经知道它是无连接无状态的,请求之间需要借助Cookie来保持会话状态,由于Locust可以自动的帮我们在请求之间关联Cookie,我们将销售出库的所有请求都放在“class UserBehavior(TaskSet)”中,这也是整个测试脚本的核心所在,下面将介绍整个脚本实现与运行流程

1.请求分析


首先我们分析一下用户从界面上完成一笔销售出库需要进行的几个主要步骤。

(1) 使用正确的用户名和密码登录成功。

(2) 点击销售出库的链接。

(3) 输入商品条码,点击“确认”。

(4) 输入会员电话,点击“查询会员信息”。

(5) 点击“确认收款”,操作完成。


20190409_150716_611.png


那么从协议层面上是不是也是对应的这些请求呢?登录请求前面已经实现过,自不必说。点击“销售出库”链接也很容易知道是一个普通的GET请求,暂且跳过。这里我们主要通过工具从输入商品条码开始捕获分析。


(1)输入商品条码是一个POST请求,分别列出请求和响应的重要信息,请求的正文是商品条码,请求的正文则将商品的日期、货号、名称、条码、单价以JSON数据格式返回。


POST http://localhost:8080/WoniuSales/sell/barcode HTTP/1.1

...

barcode=1001

HTTP/1.1 200 OK

...

[{"createtime":"<option value='60'>尺码:60,剩余:80件</option><option value='70'>尺码:70,剩余:80件</option><option value='80'>尺码:80,

剩余:80件</option>##2.0##78","goodsserial":"M8Q9066C","goodsname":"人字呢背心裙","barcode":"1001","unitprice":239.0}]


(2)接下来输入会员电话,请求正文为电话号码,响应正文为该会员的详细信息,仍然以JSON格式返回。


POST http://localhost:8080/WoniuSales/customer/query HTTP/1.1

...

customerphone=186836668866

HTTP/1.1 200 OK

...

[{"childsex":"女", "childdate":"2015-12-31","creditcloth":2136,"creditkids":500, "createtime":"2017-10-01 15:40:06","customerphone":"186836668866", "customerid":1, "credittotal":2636, "customername":"某某","updatetime":"2018-01-05 20:39:07", "userid":1}]


这两个请求目前看起来没有太多的关联,我们继续往下分析。


(3) 点击确认收款,预想中应该是发送一个POST请求,但通过工具的捕获发现实际上是两个请求。

第一个请求是将会员的信息作为请求参数,得到的响应正文代表新增成功的会员支付记录编号。


POST http://localhost:8080/WoniuSales/sell/summary HTTP/1.1

...

customerphone=186836668866&paymethod=现金&totalprice=186&creditratio=2.0 &creditsum=372&tickettype=无&ticketsum=0&oldcredit=2636

HTTP/1.1 200 OK

...

240


第二个请求则将所购买商品的信息作为请求参数,响应正文“pay-success”表示付款成功。


POST http://localhost:8080/WoniuSales/sell/detail HTTP/1.1

...

sellsumid=239&barcode=1001&goodsserial=M8Q9066C&goodsname=人字呢背心裙&goodssize=60&unitprice=239&discountratio=78&discountprice=186.42&buyquantity=1&subtotal=186.42

HTTP/1.1 200 OK

...

pay-successful


为什么会出现这种情况?从用户界面的角度来说,只进行了一次鼠标点击的操作,从这个系统设计的角度来说,付款成功后需要对数据库中的会员支出表和销售表各插入一条数据,这样才能保证后续使用时数据的一致性。所以这里也是提醒大家,界面只是一个给用户的呈现方式,其本质是协议的之间的数据传输,我们要理解其中的区别。

 

2.请求关联


要完成一笔销售出库,需要向服务器发起多次请求,并且后续的请求还要依赖前面请求返回的数据,这里就涉及到请求之间的数据关联。以新增会员支付记录的请求为例,实现成功对http://localhost:8080/WoniuSales/sell/summary发送请求。从上一节的分析可知,该请求的参数有8个,通过系统设计文档理解每个参数的意义。


20190409_150729_765.png


(1) customerphone:客户电话,与http://localhost:8080/WoniuSales/customer/query请求时的参数的值保持一致即可。

(2) paymethod:支付方式,由用户选择,这里为了方便我们固定为“现金”即可。

(3) totalprice:支付总价,需要取出http://localhost:8080/WoniuSales/sell/barcode响应中的unitprice字段,将其乘以折扣,再乘以数量计算得出。我们默认折扣都为7.8折,且数量均为1,那么totalprice的值为unitprice*0.78。

(4) creditratio:积分倍数.,设置为固定的2.0即可。

(5) creditsum:新增积分,等于unitprice乘以creditratio。

(6) tickettype:优惠券编号或原因,设置为无。

(7) ticketsum:优惠金额,设置为0。

(8) oldcredit:初识积分,来源于http://localhost:8080/WoniuSales/customer/query响应里的credittotal的值。

 

发送请求的8个参数中,totalprice和oldcredit是需要借助上一个请求中的响应数据来解析的。实现过程如下:

先实现一个根据编号获取商品信息的请求,提前构造好商品编码barcode,发送POST请求即可。

 

from locust import HttpLocust,TaskSet,task

class UserBehavior(TaskSet):

    @task

    def getGoods(self):

        body = {'barcode':'1001'}

        res = self.client.post('/WoniuSales/sell/barcode',data=body)

        print(res.text)

class WebSite(HttpLocust):

    task_set = UserBehavior

    min_wait = 1000

    max_wait = 3000


依次启动Locust并运行测试,控制台将回输出如下的响应信息,结合响应头的里的正文类型字段“Content-Type: application/json;charset=UTF-8”可知,这是一种JSON格式的数据,需要进行特殊的处理。


[2018-06-02 20:25:06,859] Teacher-Chennan/INFO/stdout: [{"createtime":"<option v

alue='60'>尺码:60,剩余:79件</option><option value='70'>尺码:70,剩余:80件</option

><option value='80'>尺码:80,剩余:80件</option>##2.0##78","goodsserial":"M8Q9066C

","goodsname":"人字呢背心裙","barcode":"1001","unitprice":239.0}]


整合后的代码如下:


(1) 导入程序需要用到的locus模块和json模块;

(2) 在on_start方法中实现了登录的操作,以便后续操作能正常进行;

(3) 在getGoods方法中发送了一个获取商品信息的POST请求,并使用loads方法对响应的JSON数据进行解析,由于解析后的数据是一个字典与列表的嵌套,先访问此列表中的第一个元素得到字典,再通过键名得到对应的值,最后乘以折扣0.78并转换为整型,将价格返回给调用者。

(4) 实现getCustomer方法,与第三步思路完全一样。

(5) 实现task任务doStart,利用getGoods和getCustomer方法的返回值构造请求正文body并发送。

(6) 实现WebSite类。


from locust import HttpLocust,TaskSet,task

import json

class UserBehavior(TaskSet):

    # 预先登录

    def on_start(self):

        body = {'username':'admin','password':'admin123','verifycode':'0000'}

        self.client.post("/WoniuSales/user/login",body)

    def getGoods(self):

        body = {'barcode':'1001'}

        res = self.client.post('/WoniuSales/sell/barcode',data=body)

        # 解析JSON数据

        newData = json.loads(res.text)

        # 提取unitprice字段的值进行计算

        totalPrice = int(newData[0]['unitprice'] * 0.78)

        return totalPrice

 

    def getCustomer(self):

        body = {'customerphone':'186836668866'}

        res = self.client.post('/WoniuSales/customer/query',data=body)

        newData = json.loads(res.text)

        oldcredit = int(newData[0]['credittotal'])

        return oldcredit

    @task

    def doStart(self):

        body ={'customerphone':'186836668866','paymethod':'现金',\

               'totalprice':self.getGoods(),'creditratio':'2.0',\

               'creditsum':'372','tickettype':'无','ticketsum':'0',\

               'oldcredit':self.getCustomer()}

        res = self.client.post('/WoniuSales/sell/summary',data=body)

        print(res.text)

class WebSite(HttpLocust):

    task_set = UserBehavior

    min_wait = 1000

    max_wait = 3000


启动Locust,打开WEB端设置模拟用户数和每秒用户生成数均为1,执行测试。查看控制台,不断输出新增成功的编号,脚本实现正确。


[2018-06-02 20:45:46,142] Teacher-Chennan/INFO/stdout: 240

[2018-06-02 20:45:46,145] Teacher-Chennan/INFO/stdout:

[2018-06-02 20:45:48,570] Teacher-Chennan/INFO/stdout: 241

[2018-06-02 20:45:48,570] Teacher-Chennan/INFO/stdout:

[2018-06-02 20:45:50,133] Teacher-Chennan/INFO/stdout: 242

[2018-06-02 20:45:50,134] Teacher-Chennan/INFO/stdout:

[2018-06-02 20:45:52,777] Teacher-Chennan/INFO/stdout: 243

[2018-06-02 20:45:52,777] Teacher-Chennan/INFO/stdout:


相对来说,这里的请求关联并不复杂,在其他更加复杂的系统业务中,我们可能需要将上一个请求的响应做进一步的解析处理,甚至会用到正则表达式去匹配响应的一部分,提供给后续的请求使用。不管怎么样,本质都是相通的,无非就是将数据通过协议进行来回的传递使用。

 

3.脚本整合


在第四章的HTTP协议部分,已经知道它是无连接无状态的,请求之间需要借助Cookie来保持会话状态,由于Locust可以自动的帮我们在请求之间关联Cookie,我们将销售出库的所有请求都放在“class UserBehavior(TaskSet)”中,这也是整个测试脚本的核心所在,下面将介绍整个脚本实现与运行流程。


(1)首先导入所需要的模块,random模块用于后面随机生成数据,json模块用于对JSON格式的响应进行解析。


from locust import HttpLocust,TaskSet,task

import random

import json


(2)实现UserBehavior 类中的on_start方法,预先将用户登录、商品编码、会员电话数据存放在列表中,便于后续进行随机处理。


class UserBehavior(TaskSet):

def on_start(self):

        self.userData = [['admin','admin123'], ['wangwu','ww123'], ['dy','DY123']]

        self.barcodeData = ['1001','1002','1003']

        self.phoneData = ['18682558655','15983123450','15812345678','13512345303']


(3)首先实现了randomValue方法,作用是传入一个列表,随机返回列表中的某一个元素;然后分别实现了三个方法,调用randomValue方法对上面的三种数据进行随机的获取。


def randomValue(self, arr):

        r = random.randint(0,10000)

        index = r % len(arr)

        return arr[index]

 

    def randomUser(self):

        return self.randomValue(self.userData)

 

    def randomBarcode(self):

        return self.randomValue(self.barcodeData)

 

    def randomPhone(self):

        return self.randomValue(self.phoneData)


(4)实现了登录、获取商品、获取会员、添加会员支付记录的四个请求。需要特别说明的两点:第一,getGoods和getCustomer方法由于返回的响应式JSON数据格式,所以进行了相应的解析,而postCustomer方法则不需要;第二,postCustomer方法有三个参数,因为会员电话、支付总额、原始积分的数据都来源于前两个值。


    def doLogin(self):

        userInfo = self.randomUser()

        body = {'username':userInfo[0], 'password':userInfo[1], 'verifycode': '0000'}

        self.client.post("/WoniuSales/user/login",data=body)

        print('----USER:' + body['username'])

        print('----PASS:' + body['password'])

 

    def getGoods(self):

        body = {'barcode': self.randomBarcode()}

        res =self.client.post('/WoniuSales/sell/barcode',data=body)

        newData = json.loads(res.text)

        return newData[0]

 

    def getCustomer(self):

        body = {'customerphone':self.randomPhone()}

        res = self.client.post('/WoniuSales/customer/query',data=body)

        newData = json.loads(res.text)

        return newData[0]

 

    def postCustomer(self, customerPhone, totalPrice, oldcredit):

        # totalPrice = int(getGoods()['unitprice'] * 0.78)

        # oldcredit = getCustomer()['credittotal']

        body ={'customerphone':customerPhone,'paymethod':'现金',\

               'totalprice':totalPrice,'creditratio':'2.0',\

               'creditsum':'372','tickettype':'无','ticketsum':'0',\

               'oldcredit':oldcredit}

        res = self.client.post('http://localhost:8080/WoniuSales/sell/summary',\

data=body)

        return res.text


(5)构造实际付款请求的10个参数,大部分参数需要从前面的请求中获取。


    def getParameters(self):

        goodsResponse = self.getGoods()

        customerResponse = self.getCustomer()

        sellSumID = self.postCustomer(customerResponse['customerphone'],\

goodsResponse['unitprice'], customerResponse['credittotal'])

        barCode = goodsResponse['barcode']

        goodsSerial = goodsResponse['goodsserial']

        goodsName = goodsResponse['goodsname']

        goodsSize = 80

        unitPrice = int(goodsResponse['unitprice'])

        discountRatio = 78

        discountPrice = unitPrice * (discountRatio / 100)

        buyQuantity = 1

        subTotal = unitPrice

        body ={'sellsumid':sellSumID,'barcode':barCode,\

               'goodsserial':goodsSerial,'goodsname':goodsName,\

'goodssize':goodsSize,'unitprice':unitPrice,'discountratio':discountRatio,\

'discountprice':discountPrice,'buyquantity':buyQuantity,'subtotal':subTotal}

        return body


(6)发送实际付款的请求,在控制台输出结果,并添加检查点。


    @task

    def doSell(self):

        self.doLogin()

        res = self.client.post('/WoniuSales/sell/detail',\

data=self.getParameters(),catch_response= True)

        print('######################### result #####################')

        print(res.text)

        if 'pay-successful' in res.text:

            res.success()

        else:

            res.failure("Pay Failed.")


(7)下面是测试配置的类,设置了思考时间。


class WebSite(HttpLocust):

    task_set = UserBehavior

    min_wait = 1000

    max_wait = 3000


(8)打开控制台,成功启动Locust。


C:\Users\Administrator\PycharmProjects\Projects\C07_Locust>locust -f Start.py --

host=http://localhost:8080

[2018-06-03 03:02:53,568] FS6V6WNJF0VQTT3/INFO/locust.main: Starting web monitor

 at *:8089

[2018-06-03 03:02:53,578] FS6V6WNJF0VQTT3/INFO/locust.main: Starting Locust 0.8


(9)打开浏览器,输入http://localhost:8089/,配置运行参数。这里为了快速看到效果,设置100和10的参数组合。


20190409_150751_368.png


(10)执行一段时间后停止,总共执行了11368个请求,且没有失败,RPS的值为192.3,运行过程良好。


20190409_150800_033.png


(11)返回控制台,截取其中一段输出结果,验证了登录用户是随机的,并且销售出库的请求都执行成功。


[2018-06-03 02:10:15,426] FS6V6WNJF0VQTT3/INFO/stdout: ----USER:wangwu

[2018-06-03 02:10:15,427] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,427] FS6V6WNJF0VQTT3/INFO/stdout: ----PASS:ww123

[2018-06-03 02:10:15,427] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,478] FS6V6WNJF0VQTT3/INFO/stdout: #########################

 result #####################

[2018-06-03 02:10:15,482] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,485] FS6V6WNJF0VQTT3/INFO/stdout: pay-successful

[2018-06-03 02:10:15,491] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,722] FS6V6WNJF0VQTT3/INFO/stdout: ----USER:admin

[2018-06-03 02:10:15,724] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,726] FS6V6WNJF0VQTT3/INFO/stdout: ----PASS:admin123

[2018-06-03 02:10:15,728] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,765] FS6V6WNJF0VQTT3/INFO/stdout: #########################

 result #####################

[2018-06-03 02:10:15,767] FS6V6WNJF0VQTT3/INFO/stdout:

[2018-06-03 02:10:15,769] FS6V6WNJF0VQTT3/INFO/stdout: pay-successful

 



为了答谢大家对蜗牛学院的支持,蜗牛学院将会定期对大家免费发放干货,敬请关注蜗牛学院的官方微信。


20190320_095757_834.jpg




版权所有,转载本站文章请注明出处:蜗牛学苑, https://www.woniuxy.cn/article/242