Cut: A Simple xUnit Test Framework in Modern C++11

动机

实现Cut(C++ Unified Test Framework)的动机,请参阅:无法忍受 Google Test 的 9 个特性

灵感

Cut(C++ Unified Test Framework)是一个简单的、可扩展的、使用C\\+\\+11实现的xUnit测试框架。Cut设计灵感来自于Java社区著名的测试框架JUnit。

安装

GitHub

编译环境

支持的平台:

  • [MAC OS X] supported
  • [Linux] supported
  • [Windows] not supported

支持的编译器:

  • [CLANG] 3.4 or later.
  • [GCC] 4.8 or later.
  • [MSVC] not supported.

安装CMake

CMake的下载地址:http://www.cmake.org

安装Cut

$ git clone https://gitlab.com/horance-liu/cut.git
$ cd cut
$ mkdir build
$ cd build
$ cmake ..
$ make
$ sudo make install

测试Cut

$ cd cut/build
$ cmake -DENABLE_TEST=on ..
$ make
$ test/cut-test

破冰之旅

物理目录
quantity
├── include
│   └── quantity
├── src
│   └── quantity
└── test
│   ├── main.cpp
└── CMakeLists.txt
main函数
#include "cut/cut.hpp"

int main(int argc, char** argv)
{
    return cut::run_all_tests(argc, argv);
}
CMakeList脚本
project(quantity)

cmake_minimum_required(VERSION 2.8)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x")

include_directories(${CMAKE_CURRENT_SOURCE_DIR}/include)

file(GLOB_RECURSE all_files
src/*.cpp
src/*.cc
src/*.c
test/*.cpp
test/*.cc
test/*.c)

add_executable(quantity-test ${all_files})

target_link_libraries(quantity-test cut)
构建
$ mkdir build
$ cd build
$ cmake ..
$ make
运行
$ ./quantity-test

[==========] Running 0 test cases.
[----------] 0 tests from All Tests
[----------] 0 tests from All Tests

[==========] 0 test cases ran.
[  TOTAL   ] PASS: 0  FAILURE: 0  ERROR: 0  TIME: 0 us

体验Cut

第一个用例

#include <cut/cut.hpp>

#include "quantity/Length.h"

USING_CUM_NS

FIXTURE(LengthTest)
{
    TEST("1 FEET should equal to 12 INCH")
    {
        ASSERT_THAT(Length(1, FEET), eq(Length(12, INCH)));
    }
};

使用 Cut,只需要包含 cut.hpp 一个头文件即可。Cut 使用 Hamcrest 的断言机制,
使得断言更加统一、自然,且具有良好的扩展性;使用 USING_CUM_NS,从而可以使用 eq
cum::eq,简短明确;除非出现名字冲突,否则推荐使用简写的 eq

Length实现

// quantity/Length.h
#include "quantity/Amount.h"

enum LengthUnit
{
    INCH = 1,
    FEET = 12 * INCH,
};

struct Length
{
    Length(Amount amount, LengthUnit unit);

    bool operator==(const Length& rhs) const;
    bool operator!=(const Length& rhs) const;

private:
    const Amount amountInBaseUnit;
};
// quantity/Length.cpp
#include "quantity/Length.h"

Length::Length(Amount amount, LengthUnit unit)
  : amountInBaseUnit(unit * amount)
{
}

bool Length::operator==(const Length& rhs) const
{
    return amountInBaseUnit == rhs.amountInBaseUnit;
}

bool Length::operator!=(const Length& rhs) const
{
    return !(*this == rhs);
}
构建
$ mkdir build
$ cd build
$ cmake ..
$ make
运行
$ ./quantity-test

[==========] Running 1 test cases.
[----------] 1 tests from All Tests
[----------] 1 tests from LengthTest
[ RUN      ] LengthTest::1 FEET should equal to 12 INCH
[       OK ] LengthTest::1 FEET should equal to 12 INCH(13 us)
[----------] 1 tests from LengthTest

[----------] 1 tests from All Tests

[==========] 1 test cases ran.
[  TOTAL   ] PASS: 1  FAILURE: 0  ERROR: 0  TIME: 13 us

Fixture

FIXTURE的参数可以是任意的C\\+\\+标识符。一般而言,将其命名为CUT(Class Under Test)的名字即可。根据作用域的大小,Fixture可分为三个类别:独立的Fixture,共享的Fixture,全局的Fixture。

支持BDD风格

xUnit BDD
FIXTURE CONTEXT
SETUP BEFORE
TEARDOWN AFTER
ASSERT_THAT EXPECT

独立的Fixture

#include <cut/cut.hpp>

FIXTURE(LengthTest)
{
    Length length;

    SETUP()
    {}

    TEARDOWN()
    {}

    TEST("length test1")
    {}

    TEST("length test2")
    {}
};

执行序列为:

  1. Length 构造函数
  2. SETUP
  3. TEST("length test1")
  4. TEARDOWN
  5. Length 析构函数
  6. Length 构造函数
  7. SETUP
  8. TEST("length test2")
  9. TEARDOWN
  10. Length 析构函数

共享的Fixture

#include <cut/cut.hpp>

FIXTURE(LengthTest)
{
    Length length;

    BEFORE_CLASS()
    {}

    AFTER_CLASS()
    {}

    BEFORE()
    {}

    AFTER()
    {}

    TEST("length test1")
    {}

    TEST("length test2")
    {}
};

执行序列为:

  1. BEFORE_CLASS
  2. Length 构造函数
  3. BEFORE
  4. TEST("length test1")
  5. AFTER
  6. Length 析构函数
  7. Length 构造函数
  8. BEFORE
  9. TEST("length test2")
  10. AFTER
  11. Length 析构函数
  12. AFTER_CLASS

全局的Fixture

有时候需要在所有用例启动之前完成一次性的全局性的配置,在所有用例运行完成之后完成一次性的清理工作。Cut则使用BEFORE_ALLAFTER_ALL两个关键字来支持这样的特性。

#include <cut/cut.hpp>

BEFORE_ALL("before all 1")
{
}

BEFORE_ALL("before all 2")
{
}

AFTER_ALL("after all 1")
{
}

AFTER_ALL("after all 2")
{
}

BEFORE_ALLAFTER_ALL向系统注册Hook即可,Cut便能自动地发现它们,并执行它们。犹如C\\+\\+不能保证各源文件中全局变量初始化的顺序一样,避免在源文件之间的BEFORE_ALLAFTER_ALL设计不合理的依赖关系。

#include <cut/cut.hpp>

FIXTURE(LengthTest)
{
    Length length;

    BEFORE_CLASS()
    {}

    AFTER_CLASS()
    {}

    BEFORE()
    {}

    AFTER()
    {}

    TEST("length test1")
    {}

    TEST("length test2")
    {}
};
#include <cut/cut.hpp>

FIXTURE(VolumeTest)
{
    Volume volume;

    BEFORE_CLASS()
    {}

    AFTER_CLASS()
    {}

    BEFORE()
    {}

    AFTER()
    {}

    TEST("volume test1")
    {}

    TEST("volume test1")
    {}
};

Cut可能的一个执行序列为:

  1. BEFORE_ALL("before all 1")
  2. BEFORE_ALL("before all 2")
  3. LengthTest::BEFORE_CLASS
  4. Length构造函数
  5. LengthTest::BEFORE
  6. TEST("length test1")
  7. LengthTest::AFTER
  8. Length析构函数
  9. Length构造函数
  10. LengthTest::BEFORE
  11. TEST("length test2")
  12. LengthTest::AFTER
  13. Length析构函数
  14. LengthTest::AFTER_CLASS
  15. VolumeTest::BEFORE_CLASS
  16. Volume构造函数
  17. LengthTest::BEFORE
  18. TEST("volume test1")
  19. LengthTest::AFTER
  20. Volume析构函数
  21. Volume构造函数
  22. LengthTest::BEFORE
  23. TEST("volume test2")
  24. LengthTest::AFTER
  25. Volume析构函数
  26. VolumeTest::AFTER_CLASS
  27. AFTER_ALL("after all 2")
  28. AFTER_ALL("after all 1")

用例设计

自动标识

Cut能够自动地实现测试用例的标识功能,用户可以使用字符串来解释说明测试用例的意图,使得用户在描述用例时更加自然和方便。

#include <cut/cut.hpp>
#include "quantity/length/Length.h"

USING_CUM_NS

FIXTURE(LengthTest)
{
    TEST("1 FEET should equal to 12 INCH")
    {
        ASSERT_THAT(Length(1, FEET), eq(Length(12, INCH)));
    }

    TEST("1 YARD should equal to 3 FEET")
    {
        ASSERT_THAT(Length(1, YARD), eq(Length(3, FEET)));
    }

    TEST("1 MILE should equal to 1760 YARD")
    {
        ASSERT_THAT(Length(1, MILE), eq(Length(1760, YARD)));
    }
};

面向对象

Cut实现xUnit时非常巧妙,使得用户设计用例时更加面向对象。RobotCleaner robot在每个用例执行时都将获取一个独立的、全新的实例。

#include "cut/cut.hpp"
#include "robot-cleaner/RobotCleaner.h"
#include "robot-cleaner/Position.h"
#include "robot-cleaner/Instructions.h"

USING_CUM_NS

FIXTURE(RobotCleanerTest)
{
    RobotCleaner robot;

    TEST("at the beginning, the robot should be in at the initial position")
    {
        ASSERT_THAT(robot.getPosition(), is(Position(0, 0, NORTH)));
    }

    TEST("left instruction: 1-times")
    {
        robot.exec(left());
        ASSERT_THAT(robot.getPosition(), is(Position(0, 0, WEST)));
    }

    TEST("left instruction: 2-times")
    {
        robot.exec(left());
        robot.exec(left());
        ASSERT_THAT(robot.getPosition(), is(Position(0, 0, SOUTH)));
    }
};

函数提取

提取的相关子函数,可以直接放在Fixture的内部,使得用例与其的距离最近,更加体现类作用域的概念。

#include "cut/cut.hpp"
#include "robot-cleaner/RobotCleaner.h"
#include "robot-cleaner/Position.h"
#include "robot-cleaner/Instructions.h"

USING_CUM_NS

FIXTURE(RobotCleanerTest)
{
    RobotCleaner robot;

    void WHEN_I_send_instruction(Instruction* instruction)
    {
        robot.exec(instruction);
    }

    void AND_I_send_instruction(Instruction* instruction)
    {
        WHEN_I_send_instruction(instruction);
    }

    void THEN_the_robot_cleaner_should_be_in(const Position& position)
    {
        ASSERT_THAT(robot.getPosition(), is(position));
    }

    TEST("at the beginning")
    {
        THEN_the_robot_cleaner_should_be_in(Position(0, 0, NORTH));
    }

    TEST("left instruction: 1-times")
    {
        WHEN_I_send_instruction(left());
        THEN_the_robot_cleaner_should_be_in(Position(0, 0, WEST));
    }

    TEST("left instruction: 2-times")
    {
        WHEN_I_send_instruction(repeat(left(), 2));
        THEN_the_robot_cleaner_should_be_in(Position(0, 0, SOUTH));
    }

    TEST("left instruction: 3-times")
    {
        WHEN_I_send_instruction(repeat(left(), 3));
        THEN_the_robot_cleaner_should_be_in(Position(0, 0, EAST));
    }

    TEST("left instruction: 4-times")
    {
        WHEN_I_send_instruction(repeat(left(), 4));
        THEN_the_robot_cleaner_should_be_in(Position(0, 0, NORTH));
    }
};

断言

ASSERT_THAT

Cut只支持一种断言原语:ASSERT_THAT, 从而避免用户在选择ASSERT_EQ/ASSERT_NE, ASSERT_TRUE/ASSERT_FALSE时的困扰,使其断言更加具有统一性,一致性。

此外,ASSERT_THAT使得断言更加具有表达力,它将实际值放在左边,期望值放在右边,更加符合英语习惯。

#include <cut/cut.hpp>

FIXTURE(CloseToTest)
{
    TEST("close to double")
    {
        ASSERT_THAT(1.0, close_to(1.0, 0.5));
        ASSERT_THAT(0.5, close_to(1.0, 0.5));
        ASSERT_THAT(1.5, close_to(1.0, 0.5));
    }
};

Hamcrest

Hamcrest是Java社区一个轻量级的,可扩展的Matcher框架,曾被Kent Beck引入到JUnit框架中,用于增强断言的机制。Cut引入了Hamcrest的设计,实现了一个C\\+\\+移植版本的Hamcrest,使得Cut的断言更加具有扩展性和可读性。

结构
anything
匹配器 说明
anything 总是匹配
_ anything语法糖
#include <cut/cut.hpp>

USING_CUM_NS

FIXTURE(AnythingTest)
{
    TEST("should always be matched")
    {
        ASSERT_THAT(1, anything<int>());
        ASSERT_THAT(1u, anything<unsigned int>());
        ASSERT_THAT(1.0, anything<double>());
        ASSERT_THAT(1.0f, anything<float>());
        ASSERT_THAT(false, anything<bool>());
        ASSERT_THAT(true, anything<bool>());
        ASSERT_THAT(nullptr, anything<std::nullptr_t>());
    }

    TEST("should support _ as syntactic sugar")
    {
        ASSERT_THAT(1u, _(int));
        ASSERT_THAT(1.0f, _(float));
        ASSERT_THAT(false, _(int));
        ASSERT_THAT(nullptr, _(std::nullptr_t));
    }
};
比较器
匹配器 说明
eq 相等
ne 不相等
lt 小于
gt 大于
le 小于或等于
ge 大于或等于
#include <cut/cut.hpp>

USING_CUM_NS

FIXTURE(EqualToTest)
{
    TEST("should allow compare to integer")
    {
        ASSERT_THAT(0xFF, eq(0xFF));
        ASSERT_THAT(0xFF, is(eq(0xFF)));

        ASSERT_THAT(0xFF, is(0xFF));
        ASSERT_THAT(0xFF == 0xFF, is(true));
    }

    TEST("should allow compare to bool")
    {
        ASSERT_THAT(true, eq(true));
        ASSERT_THAT(false, eq(false));
    }

    TEST("should allow compare to string")
    {
        ASSERT_THAT("hello", eq("hello"));
        ASSERT_THAT("hello", eq(std::string("hello")));
        ASSERT_THAT(std::string("hello"), eq(std::string("hello")));
    }
};

FIXTURE(NotEqualToTest)
{
    TEST("should allow compare to integer")
    {
        ASSERT_THAT(0xFF, ne(0xEE));

        ASSERT_THAT(0xFF, is_not(0xEE));
        ASSERT_THAT(0xFF, is_not(eq(0xEE)));
        ASSERT_THAT(0xFF != 0xEE, is(true));
    }

    TEST("should allow compare to boolean")
    {
        ASSERT_THAT(true, ne(false));
        ASSERT_THAT(false, ne(true));
    }

    TEST("should allow compare to string")
    {
        ASSERT_THAT("hello", ne("world"));
        ASSERT_THAT("hello", ne(std::string("world")));
        ASSERT_THAT(std::string("hello"), ne(std::string("world")));
    }
};
修饰器
匹配器 说明
is 可读性装饰器
is_not 可读性装饰器
#include <cut/cut.hpp>

USING_CUM_NS

FIXTURE(IsNotTest)
{
    TEST("integer")
    {
        ASSERT_THAT(0xFF, is_not(0xEE));
        ASSERT_THAT(0xFF, is_not(eq(0xEE)));
    }

    TEST("string")
    {
        ASSERT_THAT("hello", is_not("world"));
        ASSERT_THAT("hello", is_not(eq("world")));

        ASSERT_THAT("hello", is_not(std::string("world")));
        ASSERT_THAT(std::string("hello"), is_not(std::string("world")));
    }
};
空指针
匹配器 说明
nil 空指针
#include <cut/cut.hpp>

USING_CUM_NS

FIXTURE(NilTest)
{
    TEST("equal_to")
    {
        ASSERT_THAT(nullptr, eq(nullptr));
        ASSERT_THAT(0, eq(NULL));
        ASSERT_THAT(NULL, eq(NULL));
        ASSERT_THAT(NULL, eq(0));
    }

    TEST("is")
    {
        ASSERT_THAT(nullptr, is(nullptr));
        ASSERT_THAT(nullptr, is(eq(nullptr)));

        ASSERT_THAT(0, is(0));
        ASSERT_THAT(NULL, is(NULL));
        ASSERT_THAT(0, is(NULL));
        ASSERT_THAT(NULL, is(0));
    }

    TEST("nil")
    {
        ASSERT_THAT((void*)NULL, nil());
        ASSERT_THAT((void*)0, nil());
        ASSERT_THAT(nullptr, nil());
    }
};
字符串
匹配器 说明
contains_string 断言是否包含子串
contains_string_ignoring_case 忽略大小写,断言是否包含子
starts_with 断言是否以该子串开头
starts_with_ignoring_case 忽略大小写,断言是否以该子串开头
ends_with 断言是否以该子串结尾
ends_with_ignoring_case 忽略大小写,断言是否以该子串结尾
#include <cut/cut.hpp>

USING_CUM_NS

FIXTURE(StartsWithTest)
{
    TEST("case sensitive")
    {
        ASSERT_THAT("ruby-cpp", starts_with("ruby"));
        ASSERT_THAT("ruby-cpp", is(starts_with("ruby")));

        ASSERT_THAT(std::string("ruby-cpp"), starts_with("ruby"));
        ASSERT_THAT("ruby-cpp", starts_with(std::string("ruby")));
        ASSERT_THAT(std::string("ruby-cpp"), starts_with(std::string("ruby")));
    }

    TEST("ignoring case")
    {
        ASSERT_THAT("ruby-cpp", starts_with_ignoring_case("Ruby"));
        ASSERT_THAT("ruby-cpp", is(starts_with_ignoring_case("Ruby")));

        ASSERT_THAT(std::string("ruby-cpp"), starts_with_ignoring_case("RUBY"));
        ASSERT_THAT("Ruby-Cpp", starts_with_ignoring_case(std::string("rUBY")));
        ASSERT_THAT(std::string("RUBY-CPP"), starts_with_ignoring_case(std::string("ruby")));
    }
};
浮点数
匹配器 说明
close_to 断言浮点数近似等于
nan 断言浮点数不是一个数字
#include <cut/cut.hpp>
#include <math.h>

USING_CUM_NS

FIXTURE(IsNanTest)
{
    TEST("double")
    {
        ASSERT_THAT(sqrt(-1.0), nan());
        ASSERT_THAT(sqrt(-1.0), is(nan()));

        ASSERT_THAT(1.0/0.0,  is_not(nan()));
        ASSERT_THAT(-1.0/0.0, is_not(nan()));
    }
};

程序选项

TestOptions::TestOptions() : desc("cut")
{
    desc.add({
        {"help,     h",   "help message"},
        {"filter,   f",   "--filter=pattern"},
        {"color,    c",   "--color=[yes|no]"},
        {"xml,      x",   "print test result into XML file"},
        {"list,     l",   "list all tests without running them"},
        {"progress, p",   "print test result in progress bar"},
        {"verbose,  v",   "verbosely list tests processed"},
        {"repeat,   r",   "how many times to repeat each test"}
    });
    
    // default value
    options["color"]  = "yes";
    options["repeat"] = "1";
}

设计与实现

核心领域

Cut整体的结构其实是一棵树,用于用例的组织和管理。

struct TestResult;

DEFINE_ROLE(Test)
{
    ABSTRACT(const std::string& getName () const);
    ABSTRACT(int countTestCases() const);
    ABSTRACT(int countChildTests() const);
    ABSTRACT(void run(TestResult&));
};

适配

如何让FIXTURE中一个普通的成员函数TEST在运行时表现为一个TestCase呢?在C++的实现中,似乎变得非常困难。Cut的设计非常简单,将TEST的元信息在编译时注册到框架,简单地使用了C++元编程的技术,及其C++11的一些特性保证,从而解决了C++社区一直未解决此问题的关键。

TEST的运行时信息由TestMethod的概念表示,其代表FIXTURE中一个普通的成员函数TEST,它们都具有同样的函数原型: void Fixture::*)(); TestMethod是一个泛型类,泛型参数是Fixture;形式化地描述为:

template <typename Fixture>
struct TestMethod
{    
    using Method = void(Fixture::*)();
};

TestCaller也是一个泛型类,它将一个TestMethod适配为一个普通的TestCase

template <typename Fixture>
struct TestCaller : TestCase
{    
    using Method = void(Fixture::*)();

    TestCaller(const std::string& name, Method method)
        : TestCase(name), fixture(0), method(method)
    {}

private:
    OVERRIDE(void setUp())
    {
        fixture = new Fixture;
        fixture->setUp();
    }

    OVERRIDE(void tearDown())
    {
        fixture->tearDown();        
        delete fixture;
        fixture = 0;
    }

    OVERRIDE(void runTest())
    {
        (fixture->*method)();
    }

private:
    Fixture* fixture;
    Method method;
};

装饰

TestDecorator其实是对Cut核心领域的一个扩展,从而保证核心领域的不变性,而使其具有最大的可扩展性和灵活性。

工厂

在编译时通过测试用例TEST的元信息的注册,使用TestFactory很自然地将这些用例自动生成出来了。因为Magallan组织用例是一刻树,TestFactory也被设计为一棵树,从而使得其与框架核心领域保持高度的一致性,更加自然、漂亮。

监听状态

Cut通过TestListener对运行时的状态变化进行监控,从而实现了Cut不同格式报表打印的变化。

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

推荐阅读更多精彩内容