参数化数据驱动

介绍

在自动化测试中,经常会遇到如下场景:

  • 测试搜索功能,只有一个搜索输入框,但有 10 种不同类型的搜索关键字;
  • 测试账号登录功能,需要输入用户名和密码,按照等价类划分后有 20 种组合情况。

这里只是随意找了两个典型的例子,相信大家都有遇到过很多类似的场景。总结下来,就是在我们的自动化测试脚本中存在参数,并且我们需要采用不同的参数去运行。

经过概括,参数基本上分为两种类型:

  • 单个独立参数:例如前面的第一种场景,我们只需要变换搜索关键字这一个参数
  • 多个具有关联性的参数:例如前面的第二种场景,我们需要变换用户名和密码两个参数,并且这两个参数需要关联组合

然后,对于参数而言,我们可能具有一个参数列表,在脚本运行时需要按照不同的规则去取值,例如顺序取值、随机取值、循环取值等等。

这就是典型的参数化和数据驱动。

参数配置概述

如需对某测试用例(testcase)实现参数化数据驱动,需要使用 Parameters 函数,定义参数名称并指定数据源取值方式。

参数名称的定义分为两种情况:

  • 独立参数单独进行定义;
  • 多个参数具有关联性的参数需要将其定义在一起,采用短横线-进行连接。

数据源指定支持三种方式:

  • 在 pytest 测试用例中直接指定参数列表:该种方式最为简单易用,适合参数列表比较小的情况
  • 通过内置的 parameterize(可简写为P)函数引用 CSV 文件:该种方式需要准备 CSV 数据文件,适合数据量比较大的情况
  • 调用 debugtalk.py 中自定义的函数生成参数列表:该种方式最为灵活,可通过自定义 Python 函数实现任意场景的数据驱动机制,当需要动态生成参数列表时也需要选择该种方式

三种方式可根据实际项目需求进行灵活选择,同时支持多种方式的组合使用。假如测试用例中定义了多个参数,那么测试用例在运行时会对参数进行笛卡尔积组合,覆盖所有参数组合情况。

使用方式概览如下:

import pytest
from httprunner import HttpRunner, Config, Step, RunRequest, RunTestCase, Parameters


class TestCaseDemo(HttpRunner):
    parameters = Parameters(
            {
                "varA": ["paramA1", "paramA2", "paramA3"], # 指定参数列表
                "varB": "${P(user_id.csv)}",  # 通过内置的 parameterize(可简写为P)函数引用 CSV 文件
                "varC": "${get_account(10)}", # 调用 debugtalk.py 中自定义的函数生成参数列表
            }
    )

    @pytest.mark.parametrize("param",parameters)
    def test_start(self, param):
        super().test_start(param)

    config = (
        Config("xxx")
        .variables(**{"varA": "configA", "varB": "configB", "varC": "configC"})
    )

    teststeps = [
        Step(
            RunRequest("step 1")
            .with_variables(**{"varA": "step1A"})
            .get("/$varA/$varB/$varC")
            .extract()
            .with_jmespath("body.data.A", "varA")
            .with_jmespath("body.data.B", "varB")
        ),
        Step(RunRequest("step 2").get("/$varA/$varB/$varC")),
    ]


if __name__ == "__main__":
    TestCaseDemo().test_start()

参数配置详解

将参数名称定义和数据源指定方式进行组合,共有 6 种形式。现分别针对每一类情况进行详细说明。

独立参数 & 直接指定参数列表

对于参数列表比较小的情况,最简单的方式是直接在 pytest 中指定参数列表内容。

例如,对于独立参数 password,参数列表为 ['aA123456','A123456',''],那么就可以按照如下方式进行配置:

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):


    parameters = Parameters({"password": ['aA123456','A123456','']})
    
    @pytest.mark.parametrize("param", parameters)
    def test_start(self, param):
        super().test_start(param)

    config = (
        Config("登陆接口")
        .base_url("http://api.nnzhp.cn")
    )

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=niuhanyang&passwd=$password")
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

进行该配置后,测试用例在运行时就会对 password 实现数据驱动,即分别使用 ['aA123456','A123456',''] 三个值运行测试用例。运行日志如下所示:

.2020-11-13 09:59:38.199 | INFO     | httprunner.runner:test_start:451 - Start to run testcase: 登陆接口, TestCase ID: bb9a8d41-32ee-4f0d-b132-d698f4764837
2020-11-13 09:59:38.201 | INFO     | httprunner.runner:__run_step:292 - run step begin: 测试步骤 >>>>>>
2020-11-13 09:59:43.842 | DEBUG    | httprunner.client:request:186 - client IP: 192.168.1.8, Port: 54137
2020-11-13 09:59:43.843 | DEBUG    | httprunner.client:request:194 - server IP: 118.24.3.40, Port: 80
2020-11-13 09:59:43.844 | DEBUG    | httprunner.client:log_print:40 - 
================== request details ==================
method   : POST
url      : http://api.nnzhp.cn/api/user/login
headers  : {
    "User-Agent": "python-requests/2.24.0",
    "Accept-Encoding": "gzip, deflate",
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "HRUN-Request-ID": "HRUN-bb9a8d41-32ee-4f0d-b132-d698f4764837-778201",
    "Content-Length": "35"
}
cookies  : {}
body     : username=niuhanyang&passwd=aA123456

2020-11-13 09:59:43.847 | DEBUG    | httprunner.client:log_print:40 - 
================== response details ==================
status_code : 200
headers  : {
    "Server": "nginx/1.10.2",
    "Date": "Fri, 13 Nov 2020 01:59:44 GMT",
    "Content-Type": "application/json",
    "Transfer-Encoding": "chunked",
    "Connection": "keep-alive",
    "Access-Control-Allow-Origin": "*",
    "X-Frame-Options": "SAMEORIGIN"
}
cookies  : {}
encoding : None
content_type : application/json
body     : {
    "error_code": 0,
    "login_info": {
        "login_time": "20201113095944",
        "sign": "c6f51cee69ef3512ab55f67d530e861b",
        "userId": 47749
    }
}

2020-11-13 09:59:43.848 | INFO     | httprunner.client:request:218 - status_code: 200, response_time(ms): 5640.27 ms, response_length: 0 bytes
2020-11-13 09:59:43.849 | INFO     | httprunner.runner:__run_step:304 - run step end: 测试步骤 <<<<<<

2020-11-13 09:59:43.849 | INFO     | httprunner.runner:test_start:460 - generate testcase log: C:\Users\Administrator\PycharmProjects\httprunner_demo\EnterpriseWeChat\logs\bb9a8d41-32ee-4f0d-b132-d698f4764837.run.log
.2020-11-13 09:59:43.862 | INFO     | httprunner.runner:test_start:451 - Start to run testcase: 登陆接口, TestCase ID: 28afaad4-5e77-4c0a-b5ed-3511bd0f1e0c
2020-11-13 09:59:43.863 | INFO     | httprunner.runner:__run_step:292 - run step begin: 测试步骤 >>>>>>
2020-11-13 09:59:43.949 | DEBUG    | httprunner.client:request:186 - client IP: 192.168.1.8, Port: 54138
2020-11-13 09:59:43.949 | DEBUG    | httprunner.client:request:194 - server IP: 118.24.3.40, Port: 80
2020-11-13 09:59:43.950 | DEBUG    | httprunner.client:log_print:40 - 
================== request details ==================
method   : POST
url      : http://api.nnzhp.cn/api/user/login
headers  : {
    "User-Agent": "python-requests/2.24.0",
    "Accept-Encoding": "gzip, deflate",
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "HRUN-Request-ID": "HRUN-28afaad4-5e77-4c0a-b5ed-3511bd0f1e0c-783865",
    "Content-Length": "34"
}
cookies  : {}
body     : username=niuhanyang&passwd=A123456

2020-11-13 09:59:43.951 | DEBUG    | httprunner.client:log_print:40 - 
================== response details ==================
status_code : 200
headers  : {
    "Server": "nginx/1.10.2",
    "Date": "Fri, 13 Nov 2020 01:59:44 GMT",
    "Content-Type": "application/json",
    "Transfer-Encoding": "chunked",
    "Connection": "keep-alive",
    "Access-Control-Allow-Origin": "*",
    "X-Frame-Options": "SAMEORIGIN"
}
cookies  : {}
encoding : None
content_type : application/json
body     : {
    "error_code": 3007,
    "msg": "\u7528\u6237\u540d/\u5bc6\u7801\u9519\u8bef\uff01"
}

2020-11-13 09:59:43.951 | INFO     | httprunner.client:request:218 - status_code: 200, response_time(ms): 83.78 ms, response_length: 0 bytes
2020-11-13 09:59:43.952 | INFO     | httprunner.runner:__run_step:304 - run step end: 测试步骤 <<<<<<

2020-11-13 09:59:43.952 | INFO     | httprunner.runner:test_start:460 - generate testcase log: C:\Users\Administrator\PycharmProjects\httprunner_demo\EnterpriseWeChat\logs\28afaad4-5e77-4c0a-b5ed-3511bd0f1e0c.run.log
.2020-11-13 09:59:43.962 | INFO     | httprunner.runner:test_start:451 - Start to run testcase: 登陆接口, TestCase ID: 3a62c5e1-7040-4572-a2f6-437f2d2ab33d
2020-11-13 09:59:43.963 | INFO     | httprunner.runner:__run_step:292 - run step begin: 测试步骤 >>>>>>
2020-11-13 09:59:44.042 | DEBUG    | httprunner.client:request:186 - client IP: 192.168.1.8, Port: 54139
2020-11-13 09:59:44.043 | DEBUG    | httprunner.client:request:194 - server IP: 118.24.3.40, Port: 80
2020-11-13 09:59:44.043 | DEBUG    | httprunner.client:log_print:40 - 
================== request details ==================
method   : POST
url      : http://api.nnzhp.cn/api/user/login
headers  : {
    "User-Agent": "python-requests/2.24.0",
    "Accept-Encoding": "gzip, deflate",
    "Accept": "*/*",
    "Connection": "keep-alive",
    "Content-Type": "application/x-www-form-urlencoded",
    "HRUN-Request-ID": "HRUN-3a62c5e1-7040-4572-a2f6-437f2d2ab33d-783964",
    "Content-Length": "27"
}
cookies  : {}
body     : username=niuhanyang&passwd=

2020-11-13 09:59:44.045 | DEBUG    | httprunner.client:log_print:40 - 
================== response details ==================
status_code : 200
headers  : {
    "Server": "nginx/1.10.2",
    "Date": "Fri, 13 Nov 2020 01:59:44 GMT",
    "Content-Type": "application/json",
    "Transfer-Encoding": "chunked",
    "Connection": "keep-alive",
    "Access-Control-Allow-Origin": "*",
    "X-Frame-Options": "SAMEORIGIN"
}
cookies  : {}
encoding : None
content_type : application/json
body     : {
    "error_code": 3001,
    "msg": "\u5fc5\u586b\u53c2\u6570\u672a\u586b\uff01\u8bf7\u67e5\u770b\u63a5\u53e3\u6587\u6863\uff01"
}

2020-11-13 09:59:44.045 | INFO     | httprunner.client:request:218 - status_code: 200, response_time(ms): 78.79 ms, response_length: 0 bytes
2020-11-13 09:59:44.045 | INFO     | httprunner.runner:__run_step:304 - run step end: 测试步骤 <<<<<<

2020-11-13 09:59:44.046 | INFO     | httprunner.runner:test_start:460 - generate testcase log: C:\Users\Administrator\PycharmProjects\httprunner_demo\EnterpriseWeChat\logs\3a62c5e1-7040-4572-a2f6-437f2d2ab33d.run.log

可以看出,测试用例总共运行了 3 次,并且每次运行时都是采用的不同 password。

独立参数 & 引用 CSV 文件

对于已有参数列表,并且数据量比较大的情况,比较适合的方式是将参数列表值存储在 CSV 数据文件中。

对于 CSV 数据文件,需要遵循如下几项约定的规则:

  • CSV 文件中的第一行必须为参数名称,从第二行开始为参数值,每个(组)值占一行;
  • 若同一个 CSV 文件中具有多个参数,则参数名称和数值的间隔符需实用英文逗号;
  • 在 pytest 文件引用 CSV 文件时,文件路径为基于项目根目录(debugtalk.py 所在路径)的相对路径。

例如,password 的参数取值为"aA123456","A123456","" ,那么我们就可以创建 password.csv,并且在文件中按照如下形式进行描述。

password
aA123456
aA12345

然后在 pytest 测试用例文件中,就可以通过内置的 Parameterize(可简写为 P)函数引用 CSV 文件。

假设项目的根目录下有 data 文件夹,password.csv 位于其中,那么 password.csv 的引用描述如下:

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):

    """
    * 在 Parameters 中指定的参数名称必须与 CSV 文件中第一行的参数名称一致,顺序可以不一致,参数个数也可以不一致。
    * 测试用例文件中指定参数时,可以只使用部分参数,并且参数顺序无需与 CSV 文件中参数名称的顺序一致。
    """
    parameters = Parameters({"password": "${P(data/password.csv)}"})

    @pytest.mark.parametrize("param", parameters)
    def test_start(self, param):
        super().test_start(param)

    config = (
        Config("登陆接口")
        .base_url("http://api.nnzhp.cn")
    )

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=niuhanyang&passwd=$password")
            .validate()
            .assert_equal("status_code",200)
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

即 Parameters 函数的参数(CSV 文件路径)是相对于项目根目录的相对路径。当然,这里也可以使用 CSV 文件在系统中的绝对路径,不过这样的话在项目路径变动时就会出现问题,因此推荐使用相对路径的形式。

独立参数 & 引用自定义函数

对于没有现成参数列表,或者需要更灵活的方式动态生成参数的情况,可以通过在 debugtalk.py 中自定义函数生成参数列表,并在 pytest 引用自定义函数的方式。

例如,若需对 password 进行参数化数据驱动,那么就可以在 debugtalk.py 中定义一个函数,返回参数列表。

# debugtalk.py

def get_password():
    return [
        {"password":"aA123456"},
        {"password":"A123456"},
        {"password":""}
    ]

然后,在 pytest 的 Parameters 中就可以通过调用自定义函数的形式来指定数据源。

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):

    @pytest.mark.parametrize("param", Parameters({"password": "${get_password()}"}))
    def test_start(self, param):
        super().test_start(param)

    config = Config("登陆接口").base_url("http://api.nnzhp.cn")

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=niuhanyang&passwd=$password")
            .validate()
            .assert_equal("status_code",200)
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

另外,通过函数的传参机制,还可以实现更灵活的参数生成功能,在调用函数时指定需要生成的参数个数。

关联参数 & 直接指定参数列表

对于具有关联性的多个参数,例如 username 和 password,那么就可以按照如下方式进行配置:

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):

    parameters = Parameters({"password-error_code": [["aA123456","0"],["A123456","3007"],["","3001"]]})

    @pytest.mark.parametrize("param", parameters)
    def test_start(self, param):
        super().test_start(param)

    config = (
        Config("登陆接口")
        .base_url("http://api.nnzhp.cn")
    )

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=niuhanyang&passwd=$password")
            .validate()
            .assert_string_equals("body.error_code","$error_code")
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

进行该配置后,测试用例在运行时就会对 password 和 error_code 实现数据驱动,即分别使用
{"error_code": "0", "password": "aA123456"}、
{"error_code": "3007", "password": "A123456"}、
{"error_code": "3001", "password": ""}
运行 3 次测试,并且保证参数值总是成对使用。

关联参数 & 引用 CSV 文件

对于具有关联性的多个参数,例如 username 和 password,那么就可以创建 username_password_errorCode.csv,并在文件中按照如下形式进行描述。

username,password,error_code
niuhanyang,aA123456,0
niuhanyang,A123456,3007
niuhanyang,,3001

然后在 pytest 测试用例文件中,就可以通过内置的 parameterize(可简写为 P)函数引用 CSV 文件。

假设项目的根目录下有 data 文件夹,username_password_errorCode.csv 位于其中,那么 username_password_errorCode.csv 的引用描述如下:

# file_name:login_test.py

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):
    
    """
    * 在 Parameters 中指定的参数名称必须与 CSV 文件中第一行的参数名称一致,顺序可以不一致,参数个数也可以不一致。
    * 测试用例文件中指定参数时,可以只使用部分参数,并且参数顺序无需与 CSV 文件中参数名称的顺序一致。
    """
    parameters = Parameters({"username-password-error_code": "${P(data/username_password_errorCode.csv)}"})
    
    @pytest.mark.parametrize("param", parameters)
    def test_start(self, param):
        super().test_start(param)

    config = (
        Config("登陆接口")
        .base_url("http://api.nnzhp.cn")
    )

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=$username&passwd=$password")
            .validate()
            .assert_string_equals("body.error_code","$error_code")
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

关联参数 & 引用自定义函数

对于具有关联性的多个参数,实现方式也类似。
例如,在 debugtalk.py 中定义函数 get_account,生成指定数量的账号密码参数列表。

# debugtalk.py
def get_account(num):
    accounts = []
    for index in range(1, num+1):
        accounts.append(
            {"username": "user%s" % index, "password": str(index) * 6},
        )

    return accounts

那么在 pytest 的 Parameters 函数中就可以调用自定义函数生成指定数量的参数列表。

# file_name:login_test.py

import pytest
from httprunner import HttpRunner,Config,Step,RunRequest,RunTestCase,Parameters

class TestCaseLogin(HttpRunner):

    @pytest.mark.parametrize("param", Parameters({"username-password": "${get_account(3)}"}))
    def test_start(self, param):
        super().test_start(param)

    config = Config("登陆接口").base_url("http://api.nnzhp.cn")

    teststeps = [
        Step(
            RunRequest("测试步骤")
            .post("/api/user/login")
            .with_headers(**{"Content-Type": "application/x-www-form-urlencoded"})
            .with_data("username=$username&passwd=$password")
            .validate()
            .assert_equal("body.error_code",3007)
        ),
    ]

if __name__ == "__main__":
    TestCaseLogin().test_start()

需要注意的是,在自定义函数中,生成的参数列表必须为 list of dict 的数据结构[如下所示],该设计主要是为了与 CSV 文件的处理机制保持一致。

[
    {"key10":"value10","key11":"value11",..."key1n":"value1n"},
    {"key20":"value20","key21":"value21",..."key2n":"value2n"},
    ...,
    ...,
    ...,
    {"keym0":"valuem0","keym1":"valuem1",..."keymn":"valuemn"}
]

参数化运行

完成以上参数定义和数据源准备工作之后,参数化运行与普通测试用例的运行完全一致。

采用 hrun 命令运行自动化测试:

hrun testcases/demo_parameters_test.py

采用 locusts 命令运行性能测试:

locusts -f testcases/demo_parameters_test.py

区别在于,自动化测试时遍历一遍后会终止执行,性能测试时每个并发用户都会循环遍历所有参数。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271