网络请求模拟小技巧

在学习了一段时间的Python语言后,咱也大概对Python的语法和逻辑有了一定的积累,接下来,可以为接口自动化测试开始尝试做一些比较简单的准备工作啦~跟着我一起来来来~

扩展库requests

一般来说接口测试都是基于HTTP和HTTPS的网络请求,Python中有很多自带原生库和扩展库均可以实现。Python模拟HTTP请求有两种方式,一种是使用httplib模块,一种是使用requests模块,我个人比较倾向于使用requests库,该库把请求的框架都搭建好了,使用简洁明了呢~

  • 使用requests

首先要安装及导入扩展库requests,咱可以使用pip来扩展第三方库,打开CMD,输入pip install requests,安装完成后,打开编辑器,练习使用requests库,废话不多说,直接上代码~

GET请求方式模拟:

import requests      #调用requests库
test_url = 'http://xxx:8080.com'      #访问接口的url地址
response = requests.get(test_url)       #发起一个请求,使用get方法
result = response.text          #读取请求返回的结果
print(result)     #打印返回的结果

POST请求方式模拟:

import requests      #调用requests库
username = 'ningxw'    #定义参数username
password = '123456'   #定义参数password
test_url = 'http://xxx:8080.com'      #访问登录接口的url地址
datalist = {'uname': username, 'pwd': password}    #将参数添加到需求post的data中
head = {"Content-Type": "application/Json"}   #定义头部
response = requests.post(test_url, datas=datalist, headers=head)   #发起一个请求,使用post方法
result = response.text          #读取请求返回的结果
print(result)   #打印返回的结果

其实我的废话是可以不用说这么多的,给你们分享一个快速学习requests的干货,传送门 -> 快速上手 - Requests ,但是我就是想要展示下我的才气啊,我要表露出我博学多才的气质!哼!!

简单的接口自动化测试用例

恭喜你啊恭喜我啊,能把上面的网络请求掌握了,咱就可以继续下一步了~因为懂得了如何向接口请求数据,那我们就可以根据返回的数据来进行断言啦,断言是接口测试的重要部分哦~

做接口测试前的准备

  • 理解业务需求

所有测试的基础,都离不开对业务的理解,接口测试也是一样的~只有搞清楚业务的需求和逻辑,咱们才可以设计出一个比较好的用例哦~(我感觉我在讲废话! )

了解业务需求,比较快速简单的做法是可以从产品经理那边要求获取一份项目的原型,一般原型里面会把项目的内容说得很清楚,里面包含了比较重要的业务逻辑~

了解代码逻辑,可以通过开发已完成的代码去理解代码逻辑和业务逻辑,显然这部分对于大部分测试包括身为菜鸟的我是一样的难,但不得不说这是一个通往更高层面的测试的必经之路,开发的代码里面会包括多个开发的沟通协调及测试人员本身的编程基础和测试思想,为了不让自己成为轻易取代的人,咱必须得努力起来呀~干呀~

  • 查看规范的接口文档

咱做接口测试,就得对接口有深入的了解,所以一份规范的接口文档会让接口自动化测试事半功倍~但是呢,要沉淀出一份规范的接口文档,对开发来说无疑是增加重复劳动力的事情,而很多开发也不大愿意想帮助测试做些什么事~(我就常常被之前公司的开发鄙视,说我不务正业,瞎搞东西,让我做好点点点工作就好!真是好气哦!! )所以说呀,凡事还是靠自己呀,要是咱掌握了代码的能力,我们就可以自己去看代码了呀,就可以自己整理出接口文档,又不用有求于人,说不定还可以被上司赏识,从此升职加薪走上人生巅峰迎娶白富美… 好了,想想就好了,踏实的工作最重要呢~~

  • 编写代码完成接口测试

噔噔噔噔~一把鼻涕一把泪的终于要来到写代码测试接口的时刻了 ~在理清了业务需求和逻辑后,我们就可以开始试着去写一些请求了,看看接口返回的内容是不是我们预期的结果,然后通过断言来判断测试的结果~在写一个较为完整的用例之前,我们还是需要先巩固Python编程,了解一下有关数组的内容,json数据格式及转换方式unittest框架等,这样你才可以在写代码的时候游刃有余噢!!

贴一下我常用的接口自动化测试代码的基本格式和内容:

import unittest       #导入unittest
import requests     #导入requests库
import json            #导入json
 
class LianXi(unittest.TestCase):              #定义一个类,类的首字母要大写哦
       def setUp(self):                                 #初始化
             self.base_url = 'http://ip:端口号/url地址'      
 
       def test_get_success(self):             #定义一个方法,切记要以test开头哦
             datalist = {'参数1': '值1', '参数2': '值2'}               #定义传参数据
             head = {"Content-Type": "application/Json"}     #定义头部
             r = requests.post(self.base_url, params=datalist, headers=head)          #传入参数
             result = json.loads(r.text)            #使用json格式返回
             self.assertEqual(result['status'], 0)      #检验返回值
             print(result)
 
if __name__ == '__main__':
      unittest.main()

以上就是我编写自动化用例的基本模板了,才疏学浅啊各位大神多多指教~ 
在编写测试代码的时候,会很纠结在post数据的时候,应该选用哪个关键字参数,也许聪明的你熟读上面我分享的干货《快速上手 - Requests》就可以搞懂,但是蠢萌的我还是没能完全掌握啊! 于是在我无数次的谷歌之后,我找到一个可以暂时解答我疑惑的回答,传送门 -> http://stackoverflow.com/questions/9733638/post-json-using-python-requests
,这部分还是需要多花点时间去学习和巩固哦~

接口自动化实践

前面啰啰嗦嗦的写了这么多基础的东西,目的就是为了让比我还菜鸟的宝宝们方便学习~到了这一章,估计也是本文的最后一章啦啦啦,咱们就来说说接口自动化的实践吧~宝宝们,带上你的大脑,跟着我一起high high high~(一言不合就想唱歌的老毛病居然犯了~ )

我现在做的项目是一个提供用户查询物流信息的系统,就给大家讲解一个最简单的增加单号”接口的例子吧~

  • 业务需求

我们把“增加单号”的业务需求逐条提炼出来~

编号

业务步骤

1

用户登录

2

输入单号

3

检验单号是否重复

4

检验单号是否超过今日追踪数量

5

增加单号成功

  • 根据业务,定义需要处理的函数

编号

函数

1

初始化数据:setup(),用来初始化一些比较常用到的数据

2

用户登录:test_getcookie() ,用来获取用户登录的cookie

3

增加单号成功:test_addtrackno_success(),用来测试增加单号接口

  • 初步的自动化框架

根据定义的函数,可以初步搭建出一个简陋的自动化框架,就是下面那一坨~

class AddTrackNo():
         def setup():
         #初始化数据
 
         def test_getcookie():
         #获取用户登录的cookie
 
         def test_addtrackno_success():
         #测试增加单号接口
 

看到这里,我知道有人想对我的设计以深深的鄙视来吐槽我,来啊造作啊,反正你又打不着我!!哈哈哈~~

  • 还是简单粗暴点,上代码吧

上代码前啊我就是想装逼说几句,如今这世道都是套路,不过只要你掌握了其中的思想和工具,那就不在怕的了!就一个字,上!!

# -*- coding: utf-8 -*-
#导入各种库
import requests
import json
import unittest
import pymssql
import xlrd
import re
 
class AddTrackNo(unittest.TestCase):
    '''“(批量)增加单号”接口'''
 
    def setUp(self):
        '''初始化数据'''
        self.user_url = 'https://xxxxxxuser/call'
        self.buyer_url = 'https://xxxxxxorder/call'
 
        '''查询数据库中登录用户的信息'''
        self.conn = pymssql.connect(host='192.xx.xx.xx', user='sa', password='123456', database='Test_Buyer1')
        cur = self.conn.cursor()
        sql = 'SELECT TrackNO FROM TrackInfo02 WHERE (Email = '123@qq.com')'
        cur.execute(sql)
        self.result = cur.fetchall()
 
    def tearDown(self):
        '''关闭数据库'''
        self.conn.close()
 
    def test_getcookie(self):
        '''用户登录,获取cookie<账号:123@qq.com>'''
        data = {"Version": "xxx",
                "Method": "Signin",
                "SourceType": xx,
                "Param":
                    {"Email": "123@qq.com",
                     "Password": "123456"}
                }
        req = requests.post(self.user_url, json=data)
        res = json.loads(req.text)
        self.assertEqual(res["Code"], 0)
        self.assertEqual(res['Json']['FEmail'], '123@qq.com')
        #print(res)
        cookie = res["Cookies"]
        return cookie       #返回用户cookie
 
    def test_addtrackno_success(self):
        '''成功_增加单号(状态码:0 保存成功)'''
 
        '''查询数据库中该用户下的单号'''
        result = self.result
 
        '''获取EXCEL表内的单号'''
        file = xlrd.open_workbook(r'C:\Users\xx\test_trackno.xlsx')
        sheet = file.sheet_by_name('单号数据')
        cols = sheet.col_values(0)
        n = len(cols)
 
        '''去除重复单号,并post增加单号'''
        index = 0
        arylist=[]
        for row in result:
            a = re.findall('[a-zA-Z0-9]', str(row))
            sj = ''.join(a)
            arylist.append(sj)
            index += 1
 
        j = 0
        for i in range(n):
            if(j == 5):
                break
            if(cols[i] not in arylist):
                if len(result)>= 40:
                    break
 
                else:
                    j += 1
                    datalist = {"Version": "xxx",
                             "Method": "AddTrackNo",
                             "Cookies": self.test_getcookie(),
                             "SourceType": xx,
                             "Param": {
                                "TrackNos": [cols[i]]}
                             }
                    req1  = requests.post(self.buyer_url, json=datalist, headers={"Content-Type": "application/Json"} )
                    res = json.loads(req1.text)
                    print(res)
                    self.assertEqual(res['Code'], 0)
                    self.assertEqual(res['Json']['Items'][0]['ResultCode'], 0)
  • 完善自动化测试用例

以上展示的就是其中一个接口的设计和实现,是不是很简单呢! 在实际的项目中肯定会遇到很多很多接口,那咱写完一个接口,肯定不想一个个的跑起来呀,那我们就可以再创建一个文件,使其做自动化跑接口用例~

Run_Tests.py代码如下:

import time, os
import sys
import unittest
from HTMLTestRunner import HTMLTestRunner     #引入HTMLTestRunner模板
 
sys.path.append('./Interface')    
test_dir = './interface'       #指定当前文件夹下的Interface目录
file = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py')    #匹配开头为test的py文件
 
if __name__=="__main__":
 
    now = time.strftime("%Y-%m-%d %H-%M-%S", time.localtime(time.time()))    # 取当前时间
    public_path = os.path.dirname(os.path.abspath(sys.argv[0]))       # 获取当前运行的.py文件所在的绝对路径
    filename = public_path + "\\Report\\" + now + "report.html"   #保存的报告路径和名称
    fp = open(filename, 'wb')
    runner = HTMLTestRunner(stream=fp,
                            title="接口自动化报告",
                            description="详细描述如下:"
                            )
    runner.run(file)     #执行测试套件
    fp.close()

运行该文件,就可以把很多很多接口用例跑起来啦~ 代码写得就跟我一样的粗糙!!不过都是新鲜出炉的哈,以后有时间,我会认认真真去规范代码,争取做个努力上进的菜鸟~(认认真真规范的代码增加啦已经 ~~~ 耶耶耶 ~~~ )

优化接口自动化用例

啦啦啦啦啦~ 努力上进的小菜鸟又花了一些时间优化了下代码结构 上面的一大部分内容已经讲解了如何搭建一个丑陋的自动化接口测试框架,但是呢,爱美嘛是人的天性,自己撸出来的代码也要美化一下呀,不然怎么对得起自己的臭美天性呢哈哈~ 来来来,一起来看看这个“整容”过程吧 ~ 嘻嘻 ~

  • 增加配置文件(conf.ini,请注意内容,有血的教训之提示!!)

conf.ini (主要配置数据库、url、用户信息、状态码等常用数据信息)

[test_db]
#数据库信息(注意不要带上引号哦,不然会报错哦!!!)
host = 192.168.xx.xx
user = sa
password = sa123
db_test_buyer = Test_Buyer1
 
[user_info]
#用户信息
email = "123@qq.com"
upassword = "123456"
 
[url]
#请求的接口地址(url不要带上引号哦,不然会报错哦!!!)
user_url = https://xxxxxxuser/call
 
[code]
#成功状态
success = 0
 
#追踪号已存在,已添加该单号
TrackNoIsExist = -11010101
 
#追踪号无效
TrackNoInvalid = -11010102
 
  • 初始化配置文件config.py(初始化并封装配置文件的数据)
import configparser    #导入configparser库,用于读取配置文件
import os 
 
class Config():
    def __init__(self):
        self.config = configparser.ConfigParser()
        self.conf_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'conf.ini')    #获取制定配置文件所在路径
        self.config.read(self.conf_path, encoding='utf-8-sig')     #读取配置文件,编码格式为utf-8-sig
 
        self.conf = {
            'host': '', 'user': '', 'password': '', 'db_test_buyer': '', 'email': '', 'upassword': '', 'user_url': '', 'success': '', 'TrackNoIsExist ': '', 'TrackNoInvalid ': ''
                        }
        def get_conf(self):
        """
        配置文件读取,并赋值给全局参数
        :return:
        """
        self.conf['host'] = self.config.get("test_db", 'host')
        self.conf['user'] = self.config.get("test_db", "user")
        self.conf['password'] = self.config.get("test_db", "password")
        self.conf['db_test_buyer'] = self.config.get("test_db", "db_test_buyer")
        . . . . . . 
        return self.conf
 
if __name__ == '__main__':
    Config()
  • 增加公共文件Common_Func.py(主要配置数据库连接、获取用户cookie等常用函数)
import Conf.config          #导入Conf文件夹下的config.py文件,用于初始化配置文件
import pymssql                #导入pymssql,用于连接SqlServer
import requests
import json
 
def Get_TrackNo():
    '''初始化数据'''
    c = Conf.config.Config().get_conf()   #调用config.py文件的get_conf()函数
 
    '''数据库配置'''
    db_host = c["host"]
    db_user = c["user"]
    db_password = c["password"]
    db_test_buyer = c["db_test_buyer"]
    db_test_user = c["db_test_user"]
 
    '''连接SqlServer数据库'''
    conn_buyer = pymssql.connect(host=db_host, user=db_user, password=db_password, database=db_test_buyer)
    cur_buyer = conn_buyer.cursor()
    sql_buyer =sql = 'SELECT TrackNO FROM TrackInfo02 WHERE (Email = '123@qq.com')'
    cur_buyer.execute(sql_buyer)
    trackno_result = cur_buyer.fetchall()
 
    '''关闭数据库连接'''
    cur_buyer.close()
    cur_user.close()
 
    return trackno_result      #返回单号列表结果
 
def Get_Cookie():
    '''用户登录,获取cookie<账号:123@qq.com>'''
    c = Conf.config.Config().get_conf()   #调用config.py文件的get_conf()函数
    user_url = c["user_url"]
    user_email =c["email"]
    user_pwd = c["upassword"]
 
    data = {"Version": "xxx",
            "Method": "Signin",
            "SourceType": xxx,
            "Param":
                {"Email": user_email,
                "Password": user_pwd }
                }
    req = requests.post(user_url, json=data)
    uid = req.cookies.get('uid')
    return uid                              #返回用户cookie
  • 修改“增加单号”接口文件Addtrackno_test.py
# -*- coding: utf-8 -*-
import requests
import json
import unittest
import xlrd
import re
import Conf.config     #导入配置文件
import Common_Func      #导入公共文件Common_Func.py
 
 
class AddTrackNo(unittest.TestCase):
    '''“(批量)增加单号”接口'''
 
    def setUp(self):
        '''初始化数据'''
 
        '''获取请求地址'''
        c = Conf.config.Config().get_conf()   #调用config.py文件的get_conf()函数
        self.buyer = c["buyer_url"]
 
        '''获取状态码'''
        self.success = c["success"]      #成功的状态码
        self.TrackNoIsExist = c["TrackNoIsExist"]      #其它状态码
 
 
        '''获取某账号下的单号列表'''
        self.trackno_result = Common_Func.Get_TrackNo()
 
        '''获取某账号的cookie值'''
        self.user_cookie = Common_Func.Get_Cookie()
 
    def tearDown(self):
        pass
 
    def test_addtrackno_success(self):
        '''成功_(批量)增加单号(状态码:0 保存成功)'''
 
        '''获取某账号下的单号列表'''
        result = self.trackno_result
 
        '''获取EXCEL表内的单号'''
        file = xlrd.open_workbook(r'C:\Users\xx\test_trackno.xlsx')
        sheet = file.sheet_by_name('Sheet4')
        cols = sheet.col_values(0)
        n = len(cols)
 
        '''去除重复单号,并post增加单号'''
        index = 0
        arylist=[]
        for row in result:
            a = re.findall('[a-zA-Z0-9]', str(row))
            sj = ''.join(a)
            arylist.append(sj)
            index += 1
 
        j = 0
        for i in range(n):
            if(j == 5):
                break
            if(cols[i] not in arylist):
                if len(result)>= 40:
                    break
 
                else:
                    j += 1
                    data1 = {"Version": "1",
                             "Method": "AddTrackNo",
                             "Cookies": self.user_cookie,
                             "SourceType": 0,
                             "Param": {
                                "TrackNos": [cols[i]]}
                             }
                    req1 = requests.post(self.buyer_url, json=data1, headers={"Content-Type": "application/Json"} )
                    res = json.loads(req1.text)
                    print(res)
                    self.assertEqual(res['Code'], self.success)        #使用配置文件数据进行断言
                    self.assertEqual(res['Json']['Items'][0]['ResultCode'], self.success)     #使用配置文件数据进行断言
  • 修改Run_Tests.py
import time, os
import sys
sys.path.append('./Interface')
 
import Conf.config   #导入配置文件
import unittest
from HTMLTestRunner import HTMLTestRunner
 
test_dir = './Interface'
 
file = unittest.defaultTestLoader.discover(test_dir, pattern='*_test.py')
 
if __name__ == '__main__':
    Conf.config.Config().get_conf()      #初始化配置文件
    now = time.strftime("%Y-%m-%d %H-%M-%S", time.localtime(time.time()))
    public_path = os.path.dirname(os.path.abspath(sys.argv[0]))
    filename = public_path + "\\Report\\" + now + "report.html"
    fp = open(filename, 'wb')
    runner = HTMLTestRunner(stream=fp,
                            title="接口自动化报告",
                            description="详细描述如下:"
                            )
    runner.run(file)
    fp.close()
  • 接口自动化测试框架结构

小伙伴们看到这么多乱糟糟的代码肯定是已经头晕眼花,想马上关闭页面放弃治疗了是吧哈哈~下面本宝宝给大家列出了本次测试框架的结构,很清晰明了辣~大家记得要在每个文件夹下加上init.py文件哦,不懂为什么要这么做的小伙伴,谷歌和百度搜搜搜就出来啦!!本宝宝脑力有限,就将就看看吧哈哈哈我不管啦!

Conf/
  |-- init.py
  |-- conf.ini 
  |-- config.py
Interface/ 
  |-- init.py 
  |-- Common_Func.py.py 
  |-- Addtrackno_test.py 
Report/ 
  |-- report.html 
init.py
Run_Tests.py 

关于接口自动化的那些事 - 基于 Python的更多相关文章

  1. python接口自动化(三十三)-python自动发邮件总结及实例说明番外篇——下(详解)

    简介 发邮件前我们需要了解的是邮件是怎么一个形式去发送到对方手上的,通俗点来说就是你写好一封信,然后装进信封,写上地址,贴上邮票,然后就近找个邮局,把信仍进去,其他的就不关心了,只是关心时间,而电子邮 ...

  2. 基于Python+Requests+Pytest+YAML+Allure实现接口自动化

    本项目实现接口自动化的技术选型:Python+Requests+Pytest+YAML+Allure ,主要是针对之前开发的一个接口项目来进行学习,通过 Python+Requests 来发送和处理H ...

  3. 基于RestAssured实现接口自动化

    RestAssured是一款强大的接口自动化框架, 旨在使用方便的DSL,简化的接口自动化. 下面是基于RestAssured扩展的一个简单框架示例, 先看看用例的风格: package testca ...

  4. 新手入门贴之基于 python 语言的接口自动化 demo 小实战

    大家好,我是正在学习接口测试的菜鸟.近期通过自己的学习,完成了一个关于测试接口的接口自动化demo.下面想跟大家分享一下,主要的思路是根据接口文档确定测试用例,并将测试用例写在excel中.因为只是小 ...

  5. 基于Python的接口自动化-01

    为什么要做接口测试 当前互联网产品迭代速度越来越快,由之前的2-3个月到个把月,再到班车制,甚至更短,每次发版之前都需要对所有功能进行回归测试,在人力资源有限的情况下,做自动化测试很有必要.由于UI更 ...

  6. 接口自动化 [授客]基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0

    基于python+Testlink+Jenkins实现的接口自动化测试框架V3.0   by:授客 QQ:1033553122     博客:http://blog.sina.com.cn/ishou ...

  7. 接口自动化 基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]

    基于python+Testlink+Jenkins实现的接口自动化测试框架[V2.0改进版]   by:授客 QQ:1033553122 由于篇幅问题,,暂且采用网盘分享的形式: 下载地址: [授客] ...

  8. 接口自动化 基于python实现的http+json协议接口自动化测试框架源码(实用改进版)

    基于python实现的http+json协议接口自动化测试框架(实用改进版)   by:授客 QQ:1033553122 欢迎加入软件性能测试交流QQ群:7156436     目录 1.      ...

  9. 基于Python+requests搭建的自动化框架-实现流程化的接口串联

    框架产生目的:公司走的是敏捷开发模式,编写这种框架是为了能够满足当前这种发展模式,用于前后端联调之前(后端开发完接口,前端还没有将业务处理完毕的时候)以及日后回归阶段,方便为自己腾出学(mo)习(yu ...

随机推荐

  1. JQuery处理DOM元素

    现有一个id为txtMyTest的元素 获取属性值 $('#id').attr('属性名'); 设置属性值 $('#id').attr('属性名','需要设置的值'); 设置多个属性 如下同时设置va ...

  2. android菜鸟学习笔记27----Fragment的简单使用

    1.Fragment的生命周期: 简单在新建一个MyFragment继承自Fragment,重写各个生命周期回调方法,各个方法中直接输出标识相关函数被调用的信息. 重写MainActivity的各个生 ...

  3. 防sql注入之参数绑定 SQL Injection Attacks and Defense

    http://php.net/manual/zh/pdo.prepared-statements.php 预处理语句与存储过程 很多更成熟的数据库都支持预处理语句的概念.什么是预处理语句?可以把它看作 ...

  4. Laravel 出现"RuntimeException inEncrypter.php line 43: The only supported ciphers are AES-128-CBC and AES-256-CBC with the correct key lengths."问题的解决办法

    如果输入命令:php artisan key:generate 还是报错 那就要从别的项目里复制一个key到.env中,然后再运行命令:composer update和php artisan key: ...

  5. 《Python 机器学习》笔记(二)

    机器学习分类算法 本章将介绍最早以算法方式描述的分类机器学习算法:感知器(perceptron)和自适应线性神经元. 人造神经元--早期机器学习概览 MP神经元 生物神经元和MP神经元模型的对应关系如 ...

  6. Log level with log4j and Spark

    Log Level Usages OFF This is the most specific, which allows no logging at all FATAL This is the mos ...

  7. Django:popup弹出框简单应用实例

    效果:在p1.html页面点击,弹出p2的弹出框,填写数据,在 popup_response页面处理数据 1.url设置 urlpatterns = patterns( url(r'^p1.html' ...

  8. dict字典常用方法总结,数据解构(解包)

    dict {'name':'holle'}字典存储大量关联型数据,可迭代的,最多只有200个键.查询数据速度非常快,符合二分查找(有100个数比如找75会先找到50然后判断,所以2^7次方7次即可找到 ...

  9. webbrowser控件——Windows下的开发利器

    首先说明,本人比较菜,做C++没多长时间. 刚开始用MFC写程序时,连个基本的字体都不会变(颜色.大小等), 索性干脆就啥也不改了,直接默认,界面就那样了,老子不管了. 心想这C++做个界面咋就那么麻 ...

  10. TIJ读书笔记03-初始化和构造器

      TIJ读书笔记03-初始化和构造器 初始化和清理是涉及安全的两个问题,如果对象不能正确的初始化会引起很多错误,比如空指针异常等,如果不能恰当及时的清理,会占用过多资源. 构造器在创建一个类的实例的 ...