测试开发笔记七(客户端测试平台)

01 | 自动遍历测试方法与常见技术介绍


背景

  • 自动化测试成本高,周期长,只能覆盖主场景
  • 业务量大,手工测试无法覆盖所有功能点

自动化遍历测试

  • code less:用例维护成本降低到最低
  • automate:尽可能的自动化覆盖回归业务

常见遍历工具与技术

  • google android原生monkey、app crawler
  • 百度smartmonkey
  • 腾讯newmonkey
  • vigossjjj smart_monkey
  • macaca的NoSmoke
  • 头条zhangzhao maxim
  • seveniruby appcrawler

02 | android monkey测试工具


优点

  • 速度非常快
  • 编码特别少

缺点

  • 就像一只猴子不受控制,随机发生、随机点击、随机输入

安装

  • android sdk自带

使用

  • 基本配置(设置事件数量)
  • 操作约束(指定app)
  • 设置事件类型和频率
  • 调试选项
  • adb shell monkey [options] 事件计数
  • 命令
    1.adb shell monkey 100对所有包随机操作
    2.adb shell monkey -p com.xueqiu.android 100对指定包
    3.adb shell monkey -p com.xueqiu.android -s 20 80重复刚才的操作
    4.adb shell monkey -p com.xueqiu.android -vv -s 20 80详细日志
    5.adb shell monkey -p com.xueqiu.android --throttle 5000 100每个事件延迟5000ms执行
    6.adb shell monkey -p com.xueqiu.android --pct-touch 10 1000时间百分比
  • 常用事件
    1.--pct-touch触摸事件(点击)
    2.--pct-motion动作时间(直线滑动)
    3.--pct-trackball轨迹事件(移动、点击、曲线滑动)
    4.--pct-majornav主要导航事件(回退按键、菜单按键)

03 | AppCrawler跨平台自动遍历测试


与其他框架额关系

  • appcrawler底层引擎
    1.appium
    2.adb
    3.macaca
    4.selenium

AppCrawler环境要求

快速启动

  • 启动appium
  • 启动模拟器或连接真机
  • 开始自动遍历
    appcrawler --capability "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias"

执行参数与配置文件

  • capbility设置:与appium完全一致
  • testcase:用于启动app后的基础测试用例
  • selectedList:遍历范围设定
  • triggerActions:特定条件触发执行动作的设置(如关闭广告)
  • 执行参数比配置文件优先级高

testcase

  • testcase完整形态
    1.given:所有的先决条件
    2.when:先决条件成立后的行为
    3.then:断言集合
  • testcase的简写形态
    1.xpath:对应when里的xpath
    2.action:对应when的action

action

  • back
  • monkey随机事件
  • 执行代码
    1.Thread.sleep(3000)
    2.driver.swipe(0.9,0.5,0.1,0.5)
    3.click
    4.longTap

自动遍历支持

  • selectedList:需要被遍历的元素范围
  • firstList:优先被选中并执行的元素
  • lastList:最后被选中并执行的元素
  • tagLimitMax:同祖先的元素最多点击次数
  • backButton:当所有元素都被点击后默认后退控件定位
  • blackList:黑名单
  • maxDepth:遍历的最大深度(每点开一个activity,深度加1)

触发器triggerActions

  • 需要特定次数的触发动作
  • 通常用于处理弹框
    1.xpath:指定具体按钮
    2.action:动作
    3.times:规则的使用次数

工作过程

  • 信息的获取(把当前app的界面下载为xml结构)
  • 获取待遍历元素
    1.遍历范围 selectedList
    2.过滤黑名单、小控件、不可见控件、blacklist
    3.重排控件顺序 firstList lastList
    4.跳过已点击+跳过限制点击的控件tagLimit
    5.根据匹配的规则执行action
  • 循环上面的步骤

日志

  • java -jar appcrawler-2.4.0-jar-with-dependencies.jar --capability "appPackage=com.xueqiu.android,appActivity=.view.WelcomeActivityAlias"
  • 当前目录生成大量丰富的图片日志
  • appcrawler.log 搜索 current index

生成配置文件模板

  • java -jar appcrawler-2.4.0-jar-with-dependencies.jar --demo 生成配置文件模板
  • demo.yml
---
pluginList: []
saveScreen: true
reportTitle: ""
resultDir: "20200518154719"
waitLoading: 500
waitLaunch: 6000
showCancel: true
maxTime: 10800
maxDepth: 10
capability:
  noReset: "true"
  fullReset: "false"
  appium: "http://127.0.0.1:4723/wd/hub"
  appPackage: "com.xueqiu.android"
  appActivity: ".view.WelcomeActivityAlias"
testcase:
  name: "TesterHome AppCrawler"
  steps:
  - xpath: "//*[@text='行情']"
    action: click
selectedList:
  - xpath: "//*[contains(@resource-id,'single_line_wrapper')]//*[@clickable='true']"
  - xpath: "//*[contains(@resource-id,'ll_main')]//*[@clickable='true']"
firstList: 
  - xpath: "//*[contains(@resource-id,'ll_main')]//*[@clickable='true']"
lastList:
- given: []
  when: null
  then: []
  xpath: "//*[@selected='true']/..//*"
  action: null
  actions: []
  times: 0
- given: []
  when: null
  then: []
  xpath: "//*[@selected='true']/../..//*"
  action: null
  actions: []
  times: 0
backButton:
- given: []
  when: null
  then: []
  xpath: "Navigate up"
  action: null
  actions: []
  times: 0
triggerActions:
- given: []
  when: null
  then: []
  xpath: "share_comment_guide_btn"
  action: null
  actions: []
  times: 0
xpathAttributes:
- "name"
- "label"
- "value"
- "resource-id"
- "content-desc"
- "instance"
- "text"
sortByAttribute:
- "depth"
- "list"
- "selected"
findBy: "default"
defineUrl: []
baseUrl: []
appWhiteList: []
urlBlackList: []
urlWhiteList: []
blackList:
- given: []
  when: null
  then: []
  xpath: ".*[0-9]{2}.*"
  action: null
  actions: []
  times: 0
beforeRestart: []
beforeElement:
- given: []
  when: null
  then: []
  xpath: "/*"
  action: "Thread.sleep(500)"
  actions: []
  times: 0
afterElement: []
afterPage: []
afterPageMax: 2
tagLimitMax: 2
tagLimit:
- given: []
  when: null
  then: []
  xpath: "??"
  action: null
  actions: []
  times: 1000
- given: []
  when: null
  then: []
  xpath: "?§Ý"
  action: null
  actions: []
  times: 1000
- given: []
  when: null
  then: []
  xpath: "share_comment_guide_btn_name"
  action: null
  actions: []
  times: 1000
assertGlobal: []

04 | 多设备管理平台STF


简介

安装

  • 拉取镜像
    1.docker pull openstf/stf:lastest
    2.docker pull sorccu/adb:lastest
    3.docker pull rethinkdb:lastest
  • 启动rethinkdb
    docker run -d --name rethinkdb -v /srv/rethinkdb:/data --net host rethinkdb rethinkdb --bind all --cache-size 8192 --http-port 8090
  • 启动stf
    docker run -d --name stf --net host openstf/stf stf local --allow-remote
  • 访问
    http://localhost:7100

远程调试真机

  • 真机打开调试模式,与STF在同一网段
  • adb -s 设备名 tcpip 5555 给设备开调试端口
  • adb -s RFCMC00LKZH shell ifconfig 查看设备的ip地址,若访问被拒绝,可在设备中直接查看
  • adb connect 192.168.0.xx:5555 重新连接设备
  • 若设备没有连接可重启adb再连接
adb kill-server
adb start-server
adb connect 192.168.0.xx:5555

05 | slenium Grid方案


简介

  • slenium grid远程运行selenium test
  • 在多个机器上并行运行selnium

优点

  • 所有测试的入口
  • 管理和控制浏览器运行的notes/环境
  • 扩展
  • 并行运行测试
  • 跨平台测试
  • 负载均衡

流程图

image.png

下载与使用

启动Hub

java -jar selenium-server-standalone-3.141.59.jar -role hub

启动Node

  • 先配置nodeConfig.json文件
{
  "capabilities":
  [
    {
      "browserName": "firefox",
      "marionette": true,
      "maxInstances": 5,
      "seleniumProtocol": "WebDriver"
    },
    {
      "browserName": "chrome",
      "maxInstances": 5,
      "seleniumProtocol": "WebDriver"
    },
    {
      "browserName": "internet explorer",
      "platform": "WINDOWS",
      "maxInstances": 1,
      "seleniumProtocol": "WebDriver"
    },
    {
      "browserName": "safari",
      "technologyPreview": false,
      "platform": "MAC",
      "maxInstances": 1,
      "seleniumProtocol": "WebDriver"
    }
  ],
  "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
  "maxSession": 5,
  "port": -1,
  "register": true,
  "registerCycle": 5000,
  "hub": "http://localhost:4444",
  "nodeStatusCheckTimeout": 5000,
  "nodePolling": 5000,
  "role": "node",
  "unregisterIfStillDownAfter": 60000,
  "downPollingLimit": 2,
  "debug": false,
  "servlets" : [],
  "withoutServlets": [],
  "custom": {}
}
  • 在不同窗口执行以下命令,可启动多个node
    java -jar selenium-server-standalone.jar -role node -nodeConfig nodeConfig.json

代码

  • 注:若想并发执行,需用多线程
from selenium.webdriver import DesiredCapabilities
from selenium.webdriver import Remote

class TestGrid:
    def test_grid(self):
        hub_url = "http://127.0.0.1:4444/wd/hub"
        capability = DesiredCapabilities.CHROME.copy()
        for i in range(1,5):
            driver = Remote(command_executor=hub_url,desired_capabilities=capability)
            driver.get("https://www.baidu.com")

06 | 基于jenkins的自动化调度


持续集成

  • 自动化冒烟测试
  • 自动化用例运行
  • 自动化遍历测试运行
  • 兼容性测试

核心依赖资源

  • 设备集群:真机、模拟器、云端设备
  • 管理平台:STF,jenkins,selenium Grid
  • 测试用例:遍历工具,测试工具

07 | app自动化测试平台实战1


参考资料

https://android.googlesource.com/platform/system/core/+/refs/tags/android-6.0.0_r5/adb/

Monkey

monkey [-p ALLOWED_PACKAGE [-p ALLOWED_PACKAGE] ...]
      [-c MAIN_CATEGORY [-c MAIN_CATEGORY] ...]
      [--ignore-crashes] [--ignore-timeouts]
      [--ignore-security-exceptions]
      [--monitor-native-crashes] [--ignore-native-crashes]
      [--kill-process-after-error] [--hprof]
      [--pct-touch PERCENT] [--pct-motion PERCENT]
      [--pct-trackball PERCENT] [--pct-syskeys PERCENT]
      [--pct-nav PERCENT] [--pct-majornav PERCENT]
      [--pct-appswitch PERCENT] [--pct-flip PERCENT]
      [--pct-anyevent PERCENT] [--pct-pinchzoom PERCENT]
      [--pct-permission PERCENT]
      [--pkg-blacklist-file PACKAGE_BLACKLIST_FILE]
      [--pkg-whitelist-file PACKAGE_WHITELIST_FILE]
      [--wait-dbg] [--dbg-no-events]
      [--setup scriptfile] [-f scriptfile [-f scriptfile] ...]
      [--port port]
      [-s SEED] [-v [-v] ...] # 伪随机数生成器的seed值,相同seed值再次执行monkey,产生相同的事件序列
      [--throttle MILLISEC] [--randomize-throttle]
      [--profile-wait MILLISEC]
      [--device-sleep-time MILLISEC]
      [--randomize-script]
      [--script-log]
      [--bugreport]
      [--periodic-bugreport]
      [--permission-target-system]
      COUNT
  • monkey命令
adb shell monkey  -p com.xueqiu.android  --throttle 500 -s 100  100
adb shell monkey  -p com.xueqiu.android  --throttle 500 --pct-touch 10 --pct-syskeys 90  100

maxim

https://github.com/zhangzhao4444/Maxim

  • 下载安装
    1.git clone https://github.com/zhangzhao4444/Maxim.git
    2.将2个jar包推送到手机
    adb push framework.jar /sdcard
    adb push monkey.jar /sdcard
    3.执行命令
    adb shell CLASSPATH=/sdcard/monkey.jar:/sdcard/framework.jar exec app_process /system/bin tv.panda.test.monkey.Monkey -p com.panda.videoliveplatform --uiautomatormix --running-minutes 60 -v -v
    4.调用关系cat /system/bin/monkey
# Script to start "monkey" on the device, which has a very rudimentary
# shell.
#
base=/system
export CLASSPATH=$base/framework/monkey.jar
trap "" HUP
exec app_process $base/bin com.android.commands.monkey.Monkey $*

appcrawler

  • github地址:https://github.com/seveniruby/AppCrawler
  • 生成配置文件
    java -jar appcrawler-2.4.0-jar-with-dependencies.jar --demo
  • 工作原理
    1.从页面的中间元素开始遍历
    2.然后从左往右遍历
    3.遍历深度(打开的activity数量)超过最大值,执行返回操作
    4.遍历同级元素(activity下的拥有相同父节点的元素)超过最大限定值,执行返回操作
  • demo.yml

testcase(前置操作)

testcase:
  name: "TesterHome AppCrawler"
  steps:
    - xpath: "我的"
      action: click
    - xpath: //*
      action: driver.swipe(0.5,0.9,0.5,0.1)
    - xpath: "设置"
      action: click

triggeraction(去掉广告,处理弹窗)

triggerActions:
   - xpath: "//*[@resource-id='com.xueqiu.android:id/iv_close']"
     action: click

selectedList

capability:
  noReset: "true"
  fullReset: "false"
  appium: "http://127.0.0.1:4723/wd/hub"
  appPackage: "com.xueqiu.android"
  appActivity: ".view.WelcomeActivityAlias"
testcase:
  name: "TesterHome AppCrawler"
  steps:
    - xpath: "//*[@resource-id='com.xueqiu.android:id/tv_search']"
      action: click
    - xpath: "//*[@resource-id='com.xueqiu.android:id/search_input_text']"
      action: "alibaba"
    - xpath: "阿里巴巴-SW"
      action: click
    - xpath: "//*[@resource-id='com.xueqiu.android:id/stockCode' and @text='09988']"
      action: click
selectedList:
  - xpath: "//*[@resource-id='com.xueqiu.android:id/tab_indicator_view']//android.widget.TextView"
  - xpath: "//*[@resource-id='com.xueqiu.android:id/small_period_container']//android.widget.TextView"
firstList: 
  - xpath: "//*[@resource-id='com.xueqiu.android:id/tab_indicator_view']//android.widget.TextView"

08 | app、web自动化测试平台实战2


selenium工具集

  • selenium remote control:selenium 1.0版本
  • selenium webdriver:selenium 2.0版本
  • selenium server
  • selenium client
  • selenium IDE
  • selenium Grid:分布式管理设备和脚本运行

环境搭建

{
    "capabilities": [{
        "deviceName": "127.0.0.1:62001",
        "version": "4.4.2",
        "maxInstances": 1,
        "platform": "ANDROID",
        "browserName": ""
    }],
    "configuration": {
        "cleanUpCycle": 2000,
        "timeout": 30000,
        "proxy": "org.openqa.grid.selenium.proxy.DefaultRemoteProxy",
        "hub": "127.0.0.1:4444/grid/register",
        "url": "http://127.0.0.1:5723/wd/hub",
        "host": "127.0.0.1",
        "port": 5723,
        "maxSession": 1,
        "register": true,
        "registerCycle": 5000,
        "hubPort": 4444,
        "hubHost": "127.0.0.1",
        "hubProtocol": "http"
    }
}

5.启动node结点
appium -p 5723 --nodeconfig appiumnode.json
adb -s 设备编号 若链接多台设备,可用"-s"指定设备编号
adb -s 设备编号 shell am start -n package/activity
6.代码

import os

from appium import webdriver


class TestGrid:
    def setup(self):
        caps = {}
        caps['platformName'] = 'Android'
        caps['platformVersion'] = '6.0'
        caps['appPackage'] = 'com.xueqiu.android'
        caps['appActivity'] = '.view.WelcomeActivityAlias'
        caps['noRest'] = True
        # udid = os.getenv("udid")
        caps['udid'] = "127.0.0.1:7555"
        # caps['udid'] = udid

        self.driver = webdriver.Remote("http://192.168.1.100:4444/wd/hub", caps)
        self.driver.implicitly_wait(10)

    def teardown(self):
        self.driver.quit()

    def test_search(self):
        self.driver.find_element_by_id("com.xueqiu.android:id/tv_search").click()
        self.driver.find_element_by_id("com.xueqiu.android:id/search_input_text").send_keys("alibaba")

7.遍历本机设备

#!/bin/bash

for i in $(adb devices | grep "devices" | awk '{print $1}')
do
        echo "start:{$i}"
        udid=$i pytest test_search.py &
done

09 | 自动化测试平台实战3-STF


STF解决的问题

  • 远程设备手动调试
  • 远程设备自动化调试

安装方式

  • mac
    brew install rethinkdb graphicsmagick zeromq protobuf yasm pkg-config
  • linux:docker
    拉取镜像
docker pull openstf/stf:latest
docker pull sorccu/adb:latest
docker pull rethinkdb:latest

启动容器(rethinkdb,openstf)

docker run -d --name rethinkdb -v /srv/rethinkdb:/data --net host rethinkdb rethinkdb --bind all --cache-size 8192 --http-port 8090
docker run -d --name stf --net host openstf/stf stf local --allow-remote

关闭容器
docker stop stf

  • windows不建议安装

搭建局域网STF

docker run -d --name stf3 --net host openstf/stf stf local --allow-remote --public-ip 192.168.0.103
docker logs -f stf3 查看日志

搭建自动化调试

  • 获取token
    STF - Setting - keys:新建keys
  • 调用接口
    1.获取用户信息:
    curl -H 'Authorization: Bearer 4eea142d8c4a4fee9f8426e7eb7602422ea04144589143a9b9bc60bc8dddc35e' http://localhost:7100/api/v1/user | jq .
    2.获取所有设备信息,并进行重要字段提取
    curl -H "Authorization: Bearer $token" http://localhost:7100/api/v1/devices | jq -c '.devices[] | [.serial,.present,.remoteConnectUrl]'
    3.获取所有可用的设备
curl -H "Authorization: Bearer $token \
" http://localhost:7100/api/v1/devices  | 
jq -c '.devices[]|[.serial,.present,.remoteConnectUrl]'| 
grep true | awk -F \" '{print $2}'

4.stf.sh

#!/usr/bin/env bash

set -uxo pipefail

STF_TOKEN=xxx
STF_URL=xxx
DEVICE_SERIAL=xxx

if [ "$DEVICE_SERIAL" == "" ]; then
    echo "Please specify device serial using ENV['DEVICE_SERIAL']"
    return 1
fi

if [ "$STF_URL" == "" ]; then
    echo "Please specify stf url using ENV['STF_URL']"
    return 1
fi

if [ "$STF_TOKEN" == "" ]; then
    echo "Please specify stf token using using ENV['STF_TOKEN']"
    return 1
fi

function get_device
{
    curl -H "Authorization: Bearer $STF_TOKEN \
    "$STF_URL/api/v1/devices  | \
    jq -c '.devices[]|[.serial,.present,.remoteConnectUrl]'| \
    grep true | awk -F \" '{print $2}'
}

# 授权
function add_device
{
    response=$(curl -X POST -H "Content-Type: application/json" \
                 -H "Authorization: Bearer $STF_TOKEN" \
                 --data "{\"serial\": \"$DEVICE_SERIAL\"}" $STF_URL/api/v1/user/devices)

    success=$(echo "$response" | jq .success | tr -d '"')
    description=$(echo "$response" | jq .description | tr -d '"')

    if [ "$success" != "true" ]; then
        echo "Failed because $description"
        return 1
    fi

    echo "Device $DEVICE_SERIAL added successfully"
}
# 将设备拉到本地
function remote_connect
{
    response=$(curl -X POST \
                 -H "Authorization: Bearer $STF_TOKEN" \
                $STF_URL/api/v1/user/devices/$DEVICE_SERIAL/remoteConnect)

    success=$(echo "$response" | jq .success | tr -d '"')
    description=$(echo "$response" | jq .description | tr -d '"')

    if [ "$success" != "true" ]; then
        echo "Failed because $description"
        return 1
    fi
    remote_connect_url=$(echo "$response" | jq .remoteConnectUrl | tr -d '"')

    adb connect $remote_connect_url

    echo "Device $DEVICE_SERIAL remote connected successfully"
}

function remove_device
{
    response=$(curl -X DELETE \
                 -H "Authorization: Bearer $STF_TOKEN" \
                $STF_URL/api/v1/user/devices/$DEVICE_SERIAL)

    success=$(echo "$response" | jq .success | tr -d '"')
    description=$(echo "$response" | jq .description | tr -d '"')

    if [ "$success" != "true" ]; then
        echo "Failed because $description"
        return 1
    fi

    echo "Device $DEVICE_SERIAL removed successfully"
}

5.同时启动多台设备

function get_device
{
  curl -H "Authorization: Bearer $STF_TOKEN" $STF_URL/api/v1/devices  | jq -c '.devices[]|[.serial,.present,.remoteConnectUrl]'
}
function get_remote_device
{
  get_device | grep true | awk -F \" {'print $4'}
}

function run
{
  get_remote_device|while read line; do { nohup adb -s $line shell monkey 1000 &};done
}

启动本地emulator设备

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