Pytest實(shí)戰(zhàn)UI測試框架

Pytest實(shí)戰(zhàn)Web測試框架

項目結(jié)構(gòu)

用例層(測試用例)
  |
Fixtures層(業(yè)務(wù)流程)
  |
PageObject層
  |
Utils實(shí)用方法層  

使用pytest-selenium

基礎(chǔ)使用

# test_baidu.py
def test_baidu(selenium):
    selenium.get('https://www.baidu.com')
    selenium.find_element_by_id('kw').send_keys('簡書 韓志超')
    selenium.find_element_by_id('su').click()

運(yùn)行

$ pytest test_baidu.py --driver=chrome

或配置到pytest.ini中

[pytest]
addopts = --driver=chrome

使用chrome options

# conftest.py
import pytest
@pytest.fixture
def chrome_options(chrome_options):  # 覆蓋原有chrome_options
    chrome_options.add_argument('--start-maximized')
    # chrome_options.add_argument('--headless')
    return chrome_options  

Page Object層

PageObject是一種典型的設(shè)計模式剂跟,通過引入頁面對象層,來專門負(fù)責(zé)各個 頁面上的元素定位及操作曹洽。用例由面向元素轉(zhuǎn)為面向頁面對象,可以大大減少元素變動引起的維護(hù)成本送淆,如下圖。


image.png

基本模型

# baidu_page.py
class BaiduPage(object):
    search_ipt_loc = ('id', 'kw')
    search_btn_loc = ('id', 'su')
    
    def __init__(self, driver):
        self.driver = driver
    
    def input_search_keyword(self, text):
        self.driver.find_element(*self.search_ipt_loc).send_keys(text)
    
    def click_search_button(self):
        self.driver.find_element(*self.search_btn_loc).click()
        
    def search(self, text):
        self.input_search_keyword(text)
        self.click_search_button()

調(diào)用方法:

# test_baidu_page.py
from baidu_page import BaiduPage

def test_baidu_page(selenium):
    baidu = BaiduPage(selenium)
    baidu.search('簡書 韓志超')

使用頁面基類

# pages/base_page.py
class BasePage(object):
    def __init__(self, driver):
        self.driver = driver
    def input(self, element_loc, text):
        element = self.driver.find_element(*element_loc)
        element.clear()
        element.send_keys(text)
    
    def click(self, element_loc):
        self.driver.find_element(*element_loc).click()
# pages/baidu_page.py
from pages.base_page import BasePage

class BaiduPage(BasePage):
    search_ipt_loc = ('id', 'kw')
    search_btn_loc = ('id', 'su')
    
    def input_search_keyword(self, text):
        self.input(self.search_ipt_loc, text)
    
    def click_search_button(self):
        self.click(self.search_btn_loc)
        
    def search(self, text):
        self.input_search_keyword(text)
        self.click_search_button()

Fixtures業(yè)務(wù)層

# conftest.py
import pytest
from pages.baidu_page import BaiduPage()

@pytest.fixture(
def baidu_page(selenium):
    return BaiduPage(selenium)

注:selenium這個fixture的scope是function級的辟拷,自定義的badiu_page不能擴(kuò)大其scope范圍阐斜。如果想使用session級別的driver,可以自己實(shí)現(xiàn)谒出。

用例層

# test_baidu_page2.py
def test_baidu_page(baidu_page):
    baidu_page.search('簡書 韓志超')
    assert '韓志超' in baidu.driver.title

步驟漸進(jìn)

用例之間不應(yīng)相互依賴邻奠,如果部分用例擁有相同的業(yè)務(wù)流程为居,如都需要,打開登錄頁->登錄->點(diǎn)擊添加商品菜單->進(jìn)入添加商品頁面
不建議使用以下方式颜骤,并使其按順序執(zhí)行。

def test_login():
   ...
  
def test_click_menu():
   ...
   
def test_add_goods():
   ...

建議對公共的步驟進(jìn)行封裝忍抽,可以使用Fixture方法的相互調(diào)用來實(shí)現(xiàn)步驟漸進(jìn),示例如下鸠项。

# conftest.py
import pytest
from pages.login_page import LoginPage
from pages.menu_page import MenuPage
from pages.add_goods_page import AddGoodsPage

@pytest.fixture(scope='session')
def login_page(selenium):
    return LoginPage(selenium)

@pytest.fixture(scope='session')
def menu_page(selenium, login_page):
    """登錄后返回菜單頁面"""
    login_page.login('默認(rèn)用戶名', '默認(rèn)密碼') # 也可以從數(shù)據(jù)文件或環(huán)境變量中讀取
    return MenuPage(selenium)
    
@pytest.fixture(scope='session')
def add_goods_page(selenium, menu_page):
    """從MenuPage跳到添加商品頁面"""
    menu_page.click_menu('商品管理', '添加新商品')
    return AddGoodsPage(selenium)
# test_ecshop.py
def test_login(login_page):
    login_page.login('測試用戶名', '測試密碼')
    assert login_page.get_login_fail_msg() is None

def test_add_goods(add_goods_page):
    add_goods_page.input_goods_name('dell電腦')
    add_goods_page.input_goods_category("電腦")
    add_goods_page.input_goods_price('3999')
    add_goods_page.submit()
    assert add_goods_page.check_success_tip() is True

使用日志

在項目中必要的輸出信息可以幫助我們顯示測試步驟的一些中間結(jié)果和快速的定位問題祟绊,雖然Pytest框架可以自動捕獲print信息并輸出屏幕或報告中楼入,當(dāng)時更規(guī)范的應(yīng)使用logging的記錄和輸出日志牧抽。
相比print, logging模塊可以分等級記錄信息。

日志等級

實(shí)用方法層扬舒、頁面對象層、Fixture業(yè)務(wù)層讲坎、用例層都可以直接使用logging來輸出日志, 使用方法。

# test_logging.py
import logging

def test_logging():
    logging.debug('調(diào)試信息')
    logging.info('步驟信息')
    logging.warning('警告信息衫画,一般可以繼續(xù)進(jìn)行')
    logging.error('出錯信息')
    try:
       assert 0
    except Exception as ex:
        logging.exception(ex)  # 多行異常追溯信息,Error級別
    logging.critical("嚴(yán)重出錯信息")

使用pytest運(yùn)行不會有任何的log信息削罩,因?yàn)镻ytest默認(rèn)只在出錯的信息中顯示W(wǎng)ARNING以上等級的日志费奸。
要開啟屏幕實(shí)時日志鲸郊,并修改log顯示等級货邓。

Log等級: NOTSET < DEBUG < INFO < WARNING(=WARN) < ERROR < CRITICAL

# pytest.ini
[pytest]
log_cli=True
log_cli_level=INFO

運(yùn)行pytest test_logging.py四濒,查看結(jié)果:

--------------------------------------------- live log call ----------------------------------------------
INFO     root:test_logging.py:5 步驟信息
WARNING  root:test_logging.py:6 警告信息职辨,一般可以繼續(xù)進(jìn)行
ERROR    root:test_logging.py:7 出錯信息
ERROR    root:test_logging.py:11 assert 0
Traceback (most recent call last):
  File "/Users/apple/Desktop/demo/test_logging.py", line 9, in test_logging
    assert 0
AssertionError: assert 0
CRITICAL root:test_logging.py:12 嚴(yán)重出錯信息

由于日志等級設(shè)置的為INFO級別戈二,因此debug的日志不會輸出舒裤。

對于不同層日志級別的使用規(guī)范觉吭,可以在實(shí)用方法層輸出debug級別的日志,如組裝的文件路徑鲜滩,文件讀取的數(shù)據(jù),執(zhí)行的sql徙硅,sql查詢結(jié)果等等。

在PageObject層輸出info級別的日志嗓蘑,如執(zhí)行某個頁面的某項操作等。
Fixtures層和用例層可以根據(jù)需要輸出一些必要的info豌汇,warning或error級別的信息。

日志格式

默認(rèn)的日志格式?jīng)]有顯示執(zhí)行時間拒贱,我們也可以自定義日志輸出格式。

# pytest.ini
...
log_cli_format=%(asctime)s %(levelname)s %(message)s
log_cli_date_format=%Y-%m-%d %H:%M:%S
  • %(asctime)s表示時間柜思,默認(rèn)為Sat Jan 13 21:56:34 2018這種格式巷燥,我們可以使用log_cli_date_format來指定時間格式赡盘。
  • %(levelname)s代表本條日志的級別
  • %(message)s為具體的輸出信息

再次運(yùn)行pytest test_logging.py缰揪,顯示為以下格式:

--------------------------------------------- live log call ----------------------------------------------
2019-11-06 21:44:50 INFO 步驟信息
2019-11-06 21:44:50 WARNING 警告信息,一般可以繼續(xù)進(jìn)行
2019-11-06 21:44:50 ERROR 出錯信息
2019-11-06 21:44:50 ERROR assert 0
Traceback (most recent call last):
  File "/Users/apple/Desktop/demo/test_logging.py", line 9, in test_logging
    assert 0
AssertionError: assert 0
2019-11-06 21:44:50 CRITICAL 嚴(yán)重出錯信息

更多日志顯示選項

  • %(levelno)s: 打印日志級別的數(shù)值
  • %(pathname)s: 打印當(dāng)前執(zhí)行程序的路徑钝腺,其實(shí)就是sys.argv[0]
  • %(filename)s: 打印當(dāng)前執(zhí)行程序名
  • %(funcName)s: 打印日志的當(dāng)前函數(shù)
  • %(lineno)d: 打印日志的當(dāng)前行號
  • %(thread)d: 打印線程ID
  • %(threadName)s: 打印線程名稱
  • %(process)d: 打印進(jìn)程ID

輸出日志到文件

在pytest.ini中添加以下配置

...
log_file = logs/pytest.log
log_file_level = debug
log_file_format = %(asctime)s %(levelname)s %(message)s
log_file_date_format = %Y-%m-%d %H:%M:%S

log_file是輸出的文件路徑,輸入到文件的日志等級定硝、格式毫目、日期格式要單獨(dú)設(shè)置蔬啡。
遺憾的是,輸出到文件的日志每次運(yùn)行覆蓋一次箱蟆,不支持追加模式。

使用Hooks

使用Hooks可以更改Pytest的運(yùn)行流程空猜,Hooks方法一般也寫在conftest.py中,使用固定的名稱坝疼。
Pytest的Hooks方法分為以下6種:

  1. 引導(dǎo)時的鉤子方法
  2. 初始化時的的鉤子方法
  3. 收集用例時的鉤子方法
  4. 測試運(yùn)行時的鉤子方法
  5. 生成報告時的鉤子方法
  6. 斷點(diǎn)調(diào)試時的鉤子方法

Pytest完整Hooks方法API,可以參考:API參考-04-鉤子(Hooks)

修改配置

以下方法演示了動態(tài)生成測試報告名裙士。

# conftest.py
import os
from datetime import datetime
def pytest_configure(config):
    """Pytest初始化時配置方法"""
    if config.getoption('htmlpath'):  # 如果傳了--html參數(shù)
        now = datetime.now().strftime('%Y%m%d_%H%M%S')
        config.option.htmlpath = os.path.join(config.rootdir, 'reports', f'report_{now}.html')

以上示例中無論用戶--html傳了什么管毙,每次運(yùn)行腿椎,都會在項目reports目錄下夭咬,生成report_運(yùn)行時間.html格式的新的報告。
pytest_configure是Pytest引導(dǎo)時的一個固定Hook方法卓舵,我們在conftest.py或用例文件中重新這個方法可以實(shí)現(xiàn)在Pytest初始化配置時,掛上我們要執(zhí)行的一些方法(因此成為鉤子方法)掏湾。
config參數(shù)是該方法的固定參數(shù),包含了Pytest初始化時的插件筑公、命令行參數(shù)、ini項目配置等所有信息匣屡。

可以使用Python的自省方法,print(config.dict)來查看config對象的所有屬性捣作。

通常,可以通過config.getoption('--html')來獲取命令行該參數(shù)項的值券躁。使用config.getini('log_file')可以獲取pytest.ini文件中配置項的值。

添加自定義選項和配置

假設(shè)我們要實(shí)現(xiàn)一個運(yùn)行完發(fā)送Email的功能嘱朽。
我們自定義一個命令行參數(shù)項--send-email,不需要參數(shù)值搪泳。當(dāng)用戶帶上該參數(shù)運(yùn)行時扼脐,我們就發(fā)送報告,不帶則不發(fā)瓦侮,運(yùn)行格式如下:

pytest test_cases/ --html=report.html --send-email

這里,一般應(yīng)配合--html先生成報告肚吏。
由于Pytest本身并沒有--send-email這個參數(shù),我們需要通過Hooks方法進(jìn)行添加罚攀。

# conftest.py
def pytest_addoption(parser):
    """Pytest初始化時添加選項的方法"""
    parser.addoption("--send-email", action="store_true", help="send email with test report")

另外,發(fā)送郵件我們還需要郵件主題斋泄、正文、收件人等配置信息魁莉。我們可以把這些信息配置到pytest.ini中,如:

# pytest.ini
...
email_subject = Test Report
email_receivers = superhin@126.com,hanzhichao@secco.com
email_body = Hi,all\n, Please check the attachment for the Test Report.

這里需要注意旗唁,自定義的配置選項需要先注冊才能使用,注冊方法如下检疫。

# conftest.py
def pytest_addoption(parser):
    ...
    parser.addini('email_subject', help='test report email subject')
    parser.addini('email_receivers', help='test report email receivers')
    parser.addini('email_body', help='test report email body')

實(shí)現(xiàn)發(fā)送Email功能

前面我們只是添加了運(yùn)行參數(shù)和Email配置参袱,我們在某個生成報告時的Hook方法中电谣,根據(jù)參數(shù)添加發(fā)送Email功能抹蚀,示例如下。

from utils.notify import Email
# conftest.py
def pytest_terminal_summary(config):
    """Pytest生成報告時的命令行報告運(yùn)行總結(jié)方法"""
    send_email = config.getoption("--send-email")
    email_receivers = config.getini('email_receivers').split(',')
    if send_email is True and email_receivers:
        report_path = config.getoption('htmlpath')
        email_subject = config.getini('email_subject') or 'TestReport'
        email_body = config.getini('email_body') or 'Hi'
        if email_receivers:
            Email().send(email_subject, email_receivers, email_body, report_path)

使用allure-pytest

allure是一款樣式十分豐富的報告框架环壤。
安裝方法:pip install allure-pytest


allure報告

參考文檔:https://docs.qameta.io/allure/#_installing_a_commandline

Allure報告包含以下幾塊:

  • Overview: 概覽
  • Categories: 失敗用例分類
  • Suites:測手套件,對應(yīng)pytest中的測試類
  • Graphs: 圖表湃崩,報告用例總體的通過狀態(tài)荧降,標(biāo)記的不同嚴(yán)重等級和執(zhí)行時間分布。
  • Timeline: 執(zhí)行的時間線
  • Behaviors: BDD行為驅(qū)動模式朵诫,按史詩、功能剪返、用戶場景
    等來標(biāo)記和組織用例。
  • Pachages: 按包目錄來查看用例

標(biāo)記用例

pytest-allure可以自動識別pytest用例的失敗脱盲、通過日缨、skip钱反、xfail等各種狀態(tài)原因匣距,并提供更多額外的標(biāo)記,來完善用例信息墨礁。

此外,allure提供許多的額外標(biāo)記來組織用例或補(bǔ)充用例信息等恩静。

標(biāo)記測試步驟

@allure.step('')

@allure.step
def func():
    pass

當(dāng)用例調(diào)用該方法時,報告中會視為一個步驟驶乾,根據(jù)調(diào)用關(guān)系識別步驟的嵌套。

為用例添加額外信息

添加附件
  • @allure.attach.file('./data/totally_open_source_kitten.png', attachment_type=allure.attachment_type.PNG)
添加標(biāo)題和描述
  • @allure.description('')
  • @allure.description_html('')
  • @allure.title("This test has a custom title")
添加鏈接级乐、issue鏈接、用例鏈接
  • @allure.link('http://...')
  • @allure.issue('B140', 'Bug描述')
  • @allure.testcase('http://...', '用例名稱')

BDD模式組織用例

  • @allure.epics('')
  • @allure.feature('')
  • @allure.story('')
  • @allure.step('')

可以按story或feature運(yùn)行

  • --allure-epics
  • --allure-features
  • --allure-stories

標(biāo)記嚴(yán)重級別

  • @allure.severity(allure.severity_level.TRIVIAL)
  • @allure.severity(allure.severity_level.NORMAL)
  • @allure.severity(allure.severity_level.CRITICAL)

通過以下方式選擇優(yōu)先級執(zhí)行

--allure-severities normal,critical

生成allure報告

pytest --alluredir=報告文件夾路徑

運(yùn)行后該文件夾下會有一個xml格式的報告文件撒轮。
這種報告文件在jenkinz中直接使用插件解析贼穆。
如果想本地查看html格式的報告题山,需要安裝allure故痊。
安裝方法:

  • Mac: brew install allure
  • CentOS: yum install allure
  • Windows: 點(diǎn)擊下載, 下載外解壓,進(jìn)入bin目錄,使用allure.bat即可慨菱。
    使用方法,生成html報告:
allure generate 生成allure報告的文件夾

Windows可以在allure的bin目錄用allure.bat generate ...

或直接啟動報告的靜態(tài)服務(wù):

allure serve 生成allure報告的文件夾

會自動彈出瀏覽器訪問生成的報告闪彼。

Pytest實(shí)戰(zhàn)APP測試框架

APP和Web同屬于UI層,我們可以使用包含Page Object模式的同樣的分層結(jié)構(gòu)备蚓。不同的是我們需要自定義driver這個Fixture。

# conftest.py
import pytest
from appium import webdriver
@pytest.fixture(scope='session')
def driver():
    caps = {
        "platformName": "Android",
        "platformVersion": "5.1.1",
        "deviceName": "127.0.0.1:62001",
        "appPackage": "com.lqr.wechat",
        "appActivity": "com.lqr.wechat.ui.activity.SplashActivity",
        "unicodeKeyboard": True,
        "resetKeyboard": True,
        "autoLaunch": False
      }
    driver = webdriver.Remote('http://127.0.0.1:4723/wd/hub', caps)
    driver.implicitly_wait(10)
    yield driver
    driver.quit()

然后用其他Fixture或用例中直接以參數(shù)形式引入driver使用即可。

# test_weixin.py
def test_weixin_login(driver):
    driver.find_element_by_xpath('//*[@text="登錄"]').click()
    ...

使用pytest-variables

通過pip install pytest-variables安裝
假如我們需要在運(yùn)行時指定使用的設(shè)備配置以及Appium服務(wù)地址二跋,我們可以把這些配置寫到一個JSON文件中,然后使用pytest-variables插件加載這些變量扎即。
caps.json文件內(nèi)容:

{
  "caps": {
    "platformName": "Android",
    "platformVersion": "5.1.1",
    "deviceName": "127.0.0.1:62001",
    "appPackage": "com.lqr.wechat",
    "appActivity": "com.lqr.wechat.ui.activity.SplashActivity",
    "unicodeKeyboard": true,
    "resetKeyboard": true,
    "autoLaunch": false
  },
  "server": "http://localhost:4723/wd/hub"
}

Fixtures中使用:

# conftest.py
...
@pytest.fixture(scope='session')
def driver(variables):
    caps = variables['caps']
    server = variables['server']
    driver = webdriver.Remote(server, caps)
    ...

運(yùn)行方法:

pytest test_weixin.py --variables caps.json

如果有多個配置可以按caps.json格式,保存多個配置文件各拷,運(yùn)行時加載指定的配置文件即可。運(yùn)行參數(shù)也可以添加到pytest.ini的addopts中烤黍。

設(shè)置和清理

為了保證每條用例執(zhí)行完不相互影響傻盟,我們可以采取每條用例執(zhí)行時啟動app,執(zhí)行完關(guān)閉app速蕊,這屬于用例方法級別的Fixture方法娘赴。
同時,由于第一條用例執(zhí)行時也會調(diào)用該Fixture啟動app诽表,這里我們需要設(shè)置默認(rèn)連接設(shè)備是不自動啟動app,即caps中配置autoLaunch=False竿奏。
在conftest.py中添加以下Fixture方法:

# conftest.py
...
@pytest.fixture(scope='function', autouse=True)
def boot_close_app(driver):
    driver.launch_app()
    yield
    driver.close_app()

其他Fixture層的頁面對象和業(yè)務(wù)封裝可以參考Web框架的模式。

項目源碼參考:https://github.com/hanzhichao/longteng17议双,略有不同。
歡迎添加作者微信:superz-han,咨詢討論技術(shù)問題伍纫。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市莹规,隨后出現(xiàn)的幾起案子泌神,更是在濱河造成了極大的恐慌良漱,老刑警劉巖欢际,帶你破解...
    沈念sama閱讀 221,548評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異损趋,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)浑槽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,497評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來桐玻,“玉大人,你說我怎么就攤上這事镊靴。” “怎么了邑闲?”我有些...
    開封第一講書人閱讀 167,990評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長州邢。 經(jīng)常有香客問我,道長量淌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,618評論 1 296
  • 正文 為了忘掉前任呀枢,我火速辦了婚禮笼痛,結(jié)果婚禮上裙秋,老公的妹妹穿的比我還像新娘。我一直安慰自己摘刑,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,618評論 6 397
  • 文/花漫 我一把揭開白布党晋。 她就那樣靜靜地躺著,像睡著了一般徐块。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上胡控,一...
    開封第一講書人閱讀 52,246評論 1 308
  • 那天,我揣著相機(jī)與錄音舞终,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛余爆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蛾方,決...
    沈念sama閱讀 40,819評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼桩砰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起亚隅,我...
    開封第一講書人閱讀 39,725評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎煮纵,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體行疏,經(jīng)...
    沈念sama閱讀 46,268評論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡终息,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,356評論 3 340
  • 正文 我和宋清朗相戀三年夺巩,在試婚紗的時候發(fā)現(xiàn)自己被綠了周崭。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,488評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡休傍,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出磨取,到底是詐尸還是另有隱情,我是刑警寧澤忙厌,帶...
    沈念sama閱讀 36,181評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站哥放,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏爹土。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,862評論 3 333
  • 文/蒙蒙 一社露、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧峭弟,春花似錦、人聲如沸脱拼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,331評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至谎柄,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間惯雳,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,445評論 1 272
  • 我被黑心中介騙來泰國打工劈猿, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拙吉,地道東北人揪荣。 一個月前我還...
    沈念sama閱讀 48,897評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像仗颈,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子挨决,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,500評論 2 359

推薦閱讀更多精彩內(nèi)容