RestAssured学习(一)

本帖内容摘抄自:https://testerhome.com/topics/7060,在此基础上增加练习笔记(在看本帖之前可以先看原文档)。
注:本文中接口均是moco的接口,请参考《Moco接口框架应用实战》了解moco基本知识。
REST Assured是一个可以简化HTTP Builder顶层 基于REST服务的测试过程的Java DSL(针对某一领域,具有受限表达性的一种计算机程序设计语言)。它支持发起POST,GET,PUT,DELETE,OPTIONS,PATCH和HEAD请求,并且可以用来验证和校对这些请求的响应信息。

静态导入方法:
pom.xml中要加入以下依赖:

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>rest-assured</artifactId>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>3.0.2</version>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>spring-mock-mvc</artifactId>
            <version>3.0.6</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
        </dependency>

推荐大家从以下的类中静态导入方法,以提高使用rest-assured的效率。

import io.restassured.RestAssured.*;
import io.restassured.matcher.RestAssuredMatchers.*;
import org.hamcrest.Matchers.*;

如果您想使用Json Schema validation 还应该静态导入这些方法:

import io.restassured.module.jsv.JsonSchemaValidator.*;

更多使用方法参阅 Json Schema Validation

如果您正在使用SpringMVC,你可以使用spring-mock-mvc 模型的Rest Assured DSL来对Spring的controller层进行单元测试。为此需要从RestAssuredMockMvc静态导入这些方法:

import io.restassured.module.mockmvc.RestAssuredMockMvc.*;

示例
例一 - JSON
假设某个get请求 (to http://localhost:8889/lotto) 返回JSON如下:

  {
    "description":"返回json接口",
    "request":{
      "uri":"/lotto",
      "method":"get"
    },
    "response":{
      "json":{
        "lotto":{
          "lottoId":5,
          "winning-numbers":[2,45,34,23,7,5,3],
          "winners":[{
            "winnerId":23,
            "numbers":[2,45,34,23,3,5]
          },{
            "winnerId":54,
            "numbers":[52,3,12,11,18,22]
          }]
        }
      }
    }
  }

REST assured可以帮您轻松地进行get请求并对响应信息进行处理。举个例子,如果想要判断lottoId的值是否等于5,你可以这样做:
在类中写一个方法,写下如下代码:

    @Test
    public void testone(){
        given()
                .when()
                .get("http://localhost:8889/lotto")
                .then()
                .body("lotto.lottoId",equalTo(5));

    }

注:lottoId必须是int类型,才能用equalTo(5),如果是string类型,用equalTo("5")
又或许您想要检查winnerId的取值包括23和54:

    @Test
    public void testtwo(){
        given()
                .when()
                .get("http://localhost:8889/lotto")
                .then()
                .body("lotto.winners.winnerId",hasItems(23,54));

    }

注意: equalTo 和 hasItems 是 Hamcrest matchers的方法,所以需要静态导入 import static org.hamcrest.Matchers.*;
注意这里的"json path"语法使用的是Groovy的GPath标注法,不要和Jayway的JsonPath语法混淆。(暂时可以不用关注)

以BigDecimal返回float和double类型
(译者注:Java在java.math包中提供的API类BigDecimal,用来对超过16位有效位的数进行精确的运算)

您可以对rest-assured和JsonPath进行配置,使之以BigDecimal返回json里的数值类型数据,而不是float或者double。可以参考下面json文本:

  {
    "description":"以BigDecimal返回float和double类型",
    "request":{
      "uri":"/price",
      "method":"get"
    },
    "response":{
      "json":{
        "price":12.12
      }
    }
  }

默认情况下您验证price字段是否等于float类型的12.12像这样:

    @Test
    public void testthree(){
        given()
                .when()
                .get("http://localhost:8889/price")
                .then()
                .body("price",is(12.12f));

    }

但是如果想用rest-assured的JsonConfig来配置返回的所有的json数值都为BigDecimal类型:

    @Test
    public void testfour(){
        given()
                .config(RestAssured.config().jsonConfig(jsonConfig().numberReturnType(BIG_DECIMAL)))
                .when()
                .get("http://localhost:8889/price")
                .then()
                .body("price",is(new BigDecimal(12.12)));

    }

因为上面moco的接口没有定义price是BigDecimal类型,所以test的接口代码会报错,实际测试中遇到返回值类型是BigDecimal的可以用这个代码。

JSON Schema validation
自从 2.1.0 版本rest-assured开始支持Json Schema validation. 举个例子,在classpath中放置以下的schema文件(译者注:idea的话可以放在resources目录下),products-schema.json:

{
  "definitions": {},
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://example.com/root.json",
  "type": "object",
  "title": "The Root Schema",
  "required": [
    "lotto"
  ],
  "properties": {
    "lotto": {
      "$id": "#/properties/lotto",
      "type": "object",
      "title": "The Lotto Schema",
      "required": [
        "lottoId",
        "winning-numbers",
        "winners"
      ],
      "properties": {
        "lottoId": {
          "$id": "#/properties/lotto/properties/lottoId",
          "type": "integer",
          "title": "The Lottoid Schema",
          "default": 0,
          "examples": [
            5
          ]
        },
        "winning-numbers": {
          "$id": "#/properties/lotto/properties/winning-numbers",
          "type": "array",
          "title": "The Winning-numbers Schema",
          "items": {
            "$id": "#/properties/lotto/properties/winning-numbers/items",
            "type": "integer",
            "title": "The Items Schema",
            "default": 0,
            "examples": [
              2,
              45,
              34,
              23,
              7,
              5,
              3
            ]
          }
        },
        "winners": {
          "$id": "#/properties/lotto/properties/winners",
          "type": "array",
          "title": "The Winners Schema",
          "items": {
            "$id": "#/properties/lotto/properties/winners/items",
            "type": "object",
            "title": "The Items Schema",
            "required": [
              "winnerId",
              "numbers"
            ],
            "properties": {
              "winnerId": {
                "$id": "#/properties/lotto/properties/winners/items/properties/winnerId",
                "type": "integer",
                "title": "The Winnerid Schema",
                "default": 0,
                "examples": [
                  23
                ]
              },
              "numbers": {
                "$id": "#/properties/lotto/properties/winners/items/properties/numbers",
                "type": "array",
                "title": "The Numbers Schema",
                "items": {
                  "$id": "#/properties/lotto/properties/winners/items/properties/numbers/items",
                  "type": "integer",
                  "title": "The Items Schema",
                  "default": 0,
                  "examples": [
                    2,
                    45,
                    34,
                    23,
                    3,
                    5
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}

您可以使用这个schema验证(/products)这个请求是否符合规范:

    @Test
    public void testfive(){
        given()
                .when()
                .get("http://localhost:8889/lotto")
                .then()
                .assertThat()
                .body(matchesJsonSchemaInClasspath("products-schema.json"));

    }

注:jsonschema 可以在https://www.jsonschema.net/网页中生成,
在resource包下创建products-schema.json,放入从网页中生成的jsonschema。

matchesJsonSchemaInClasspath 静态导入自 io.restassured.module.jsv.JsonSchemaValidator 并且我们推荐从这个类中静态导入所有的方法。

import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;

maven依赖,上面已经讲过要加入该依赖:

        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>json-schema-validator</artifactId>
            <version>3.0.2</version>
        </dependency>

JSON Schema Validation 设置项

rest-assured的json-schema-validator module使用Francis Galiegue的json-schema-validator (fge) 库来进行验证。 如果您想配置使用基础fge库,你可以像下面例子中:

    @Test
    public void testsix(){
        // Given
        JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.newBuilder().setValidationConfiguration(ValidationConfiguration.newBuilder().setDefaultVersion(DRAFTV4).freeze()).freeze();
        // When
        get("http://localhost:8889/lotto").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(jsonSchemaFactory));

    }

using方法允许您进入jsonSchemaFactory的实例,rest-assured在验证期间也会进行此操作。这种方式允许我们对验证进行细粒度的配置。

fge库也允许验证状态是 checked或者unchecked(译者注:表示不懂)。默认情况,rest-assured使用checked验证,但是如果你想要改变这种方式,您可以提供一个matcher的JsonSchemaValidatorSettings实例。举个例子:
get("/products").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json").using(settings().with().checkedValidation(false)));
这些settings方法静态导入自 JsonSchemaValidatorSettings类。

注:想要验证jsonschema,用testfive方法中写的验证就可以了。

Json Schema Validation的静态配置

现在想象下您总是使用unchecked验证,并且设置默认的json schema版本为3。与其每次都在代码里进行设置,不如静态地进行定义设置。举个例子:

    @Test
    public void testeight(){
        JsonSchemaValidator.settings = settings().with().jsonSchemaFactory(
                JsonSchemaFactory.newBuilder().setValidationConfiguration(ValidationConfiguration.newBuilder().setDefaultVersion(DRAFTV3).freeze()).freeze()).
                and().with().checkedValidation(false);

        get("http://localhost:8889/lotto").then().assertThat().body(matchesJsonSchemaInClasspath("products-schema.json"));
    }

注:DRAFTV3 会报错,DRAFTV4是可以成功的。

现在任意一个由JsonSchemaValidator导入的matcher都会使用DRAFTV3作为默认版本并且unchecked validation。

想要重置JsonSchemaValidator到默认设置仅仅需要调用reset方法:

JsonSchemaValidator.reset();

不使用rest-assured的Json Schema Validation
您也可以在不依赖rest-assured的情况下使用json-schema-validator module。如想要把json文本表示为String类型的字符串,可以这样做:

    @Test
    public void testnine(){

        String json = "{\n" +
                "        \"lotto\":{\n" +
                "          \"lottoId\":5,\n" +
                "          \"winning-numbers\":[2,45,34,23,7,5,3],\n" +
                "          \"winners\":[{\n" +
                "            \"winnerId\":23,\n" +
                "            \"numbers\":[2,45,34,23,3,5]\n" +
                "          },{\n" +
                "            \"winnerId\":54,\n" +
                "            \"numbers\":[52,3,12,11,18,22]\n" +
                "          }]\n" +
                "        }\n" +
                "      }";
        assertThat(json,matchesJsonSchemaInClasspath("products-schema.json"));
    }

匿名式的JSON根节点验证
一个JSON文本并不总是有一个命名好的根属性。这里有个验证这种JSON的例子:
[1, 2, 3]

注:这种json的返回不太好设计,moco不出这样的接口。

一个匿名的JSON根属性可以通过使用$或者空字符串作为路径来识别。举个例子,通过访问http://localhost:8889/json这个地址可以获得一个JSON文本,我们可以使用rest-assured验证:

    @Test
    public void testten(){
        given()
                .when()
                .get("http://localhost:8889/json")
                .then()
                .body("$",hasItems(1,2,3));

    }

例2 - XML
XML可以一种通过简单的方式解析。假设一个POST请求http://localhost:8080/greetXML返回:

  {
    "description":"返回内容是xml",
    "request":{
      "uri":"/getxml",
      "method":"post",
      "forms":{
        "firstName":"John",
        "lastName":"Doe"
      }
    },
    "response":{
      "text":"<?xml version=\"1.0\" encoding=\"UTF-8\" ?> <greeting>\n<firstName>John</firstName>\n<lastName>Doe</lastName>\n</greeting>",
      "headers":{
        "Content-Type":"text/xml"
      }

    }
    }

换言之,它在请求中返还了一个基于firstname和lastname请求参数的greeting节点。您可以通过rest-assured轻易地展现和解析这个例子

        //post入参为form 表单参数,返回类型为xml
        given()
                .proxy(8888)//连接代理
                .formParam("firstName", "John")
                .formParam("lastName","Doe")
                .when()
                .post("http://localhost:8889/getxml")
                .then()
                .using()
                .defaultParser(Parser.XML)//返回类型是xml格式
                .body("greeting.firstName",equalTo("John"));//校验结果

如果您想同时解析firstname和lastname可以这样做:

        //post入参为form 表单参数,返回类型为xml
        given()
                .proxy(8888)//连接代理
                .formParam("firstName", "John")
                .formParam("lastName","Doe")
                .when()
                .post("http://localhost:8889/getxml")
                .then()
                .using()
                .defaultParser(Parser.XML)//返回类型是xml格式
                .body("greeting.firstName",equalTo("John"))
                .body("greeting.lastName",equalTo("Doe"));//校验结果

或者稍微简短些:

        with().formParams("firstName", "John", "lastName", "Doe").when().post("http://localhost:8889/getxml").then().body("greeting.firstName", equalTo("John"), "greeting.lastName", equalTo("Doe"));

XML 命名空间

考虑到您需要使用io.restassured.config.XmlConfig声明一个命名空间。举个例子,有一个位于http://localhost:8080的资源namespace-example,返回如下的XML:

{
    "description":"返回内容是xml",
    "request":{
      "uri":"/getxmlwithnamespace",
      "method":"get"
    },
    "response":{
      "text":"<?xml version=\"1.0\" encoding=\"UTF-8\" ?> <foo xmlns:ns=\"http://localhost/\">\n<bar>sudo </bar>\n<ns:bar>make me a sandwich!</ns:bar>\n</foo>",
      "headers":{
        "Content-Type":"text/xml"
      }
    }
  }

可以然后声明http://localhost/这个URI并且验证其响应:

    @Test
    public void testtwelve(){
        given().
                config(RestAssured.config().xmlConfig(xmlConfig().declareNamespace("test", "http://localhost/"))).
                when().
                get("http://localhost:8889/getxmlwithnamespace").
                then().
                body("foo.bar.text()", equalTo("sudo make me a sandwich!")).
                body(":foo.:bar.text()", equalTo("sudo ")).
                body("foo.test:bar.text()", equalTo("make me a sandwich!"));
    }

这个路径语法遵循Groovy的XmlSlurper语法。注意直到2.6.0的路径语法都遵循Groovy的XmlSlurper语法。请看release notes可以获知2.6.0之前的版本语法是怎样的。

注:参考《XML的命名空间》了解本代码

XPath:
您也可以使用x-path来解析XML响应。举个例子:
moco接口:

  {
    "description":"返回内容是xml,入参类型是json",
    "request":{
      "uri":"/getxmlwithjson",
      "method":"post",
      "json":{
        "firstName":"John",
        "lastName":"Doe"
      }
    },
    "response":{
      "text":"<?xml version=\"1.0\" encoding=\"UTF-8\" ?> <greeting>\n<firstName>John</firstName>\n<lastName>Doe</lastName>\n</greeting>",
      "headers":{
        "Content-Type":"text/xml"
      }
    }
  }

验证方法:

        Map map = new HashMap();
        map.put("firstName","John");
        map.put("lastName","Doe");


        given()
                .proxy(8888)
                .body(map)
                .when()
                .post("http://localhost:8889/getxmlwithjson")
                .then()
                .body(hasXPath("/greeting/firstName[text()='John']"));

或者:

        given()
                .proxy(8888)
                .body(map)
                .when()
                .post("http://localhost:8889/getxmlwithjson")
                .then()
                .body(hasXPath("/greeting/firstName",containsString("Jo")));

在XPath表达式中使用命名空间,你需要在配置中启用这些选项:
xml:

<h:table xmlns:h="http://www.w3.org/TR/html4/">
   <h:tr>
   <h:td>Apples</h:td>
   <h:td>Bananas</h:td>
   </h:tr>
</h:table>

代码如下:此代码运行不成功,namespaceContext的实例写的不对。

    @Test
    public void testfourteen(){

        NamespaceContext namespaceContext = new NamespaceContext() {
            @Override
            public String getNamespaceURI(String prefix) {
                return null;
            }

            @Override
            public String getPrefix(String namespaceURI) {
                return null;
            }

            @Override
            public Iterator getPrefixes(String namespaceURI) {
                return null;
            }
        };


        given()
                .config(RestAssured.config().xmlConfig(xmlConfig().with().namespaceAware(true)))
                .proxy(8888)
                .when()
                .get("http://localhost:8889/getxmlwithnamespacetwo")
                .then()
                .body(hasXPath("/h:table",namespaceContext,equalTo("111")));



    }

Schema和DTD
XML响应体也可以验证为一个XML Schema (XSD)或DTD.

校验XSD是指:接口中返回xml响应体,针对xml生成xml schema,就是xsd后缀的文件,校验xsd文件和返回的xml响应体是否一致。

校验DTD是指:接口中返回的xml响应体中定义了DTD的文档规范,DTD文档是以dtd后缀的文件,校验dtd文件和返回的xml响应体的文档规范是否一致。
DTD文档是指在xml头中定义的DOCTYPE规范,比如下面的D:\Springboot\AutoTest\Chapter15\src\main\resources\XSD\mybatis-3-config.dtd路径下的mybatis-3-config.dtd文件就规范了该xml的文档规范。

<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "D:\Springboot\AutoTest\Chapter15\src\main\resources\XSD\mybatis-3-config.dtd">

XSD 例子

get("/carRecords").then().assertThat().body(matchesXsd(xsd));

xsd是xml schema definition,xml文档的结构定义。
moco的接口返回xml文档,对xml文档生成对应的xsd文档

  {
    "description":"模拟返回内容是文件",
    "request" :
    {
      "uri":"/getresponsewithfile",
      "method":"get"
    },
    "response" :
    {
      "file" : "D:/Springboot/AutoTest/Chapter15/src/main/resources/XSD/assertxml.xml"
    }
  }

xml文档内容如下:


image.png
<?xml version="1.0" encoding="UTF-8"?>
<root>
    <mail mailName="邮件客户端" mailDescription="打开本地邮件客户端发送邮件" >
        <mailServer sp="163" >
            <pop3 key="ip" value="192.168.1.1" />
            <pop3 key="port" value="1234" />
            <user key="userName" value="abcduser" />
            <user key="password" value="dfdf" />
        </mailServer>
    </mail>
</root>

生成的xsd文档内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
    <xs:element name="root">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="mail"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>
    <xs:element name="mail">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="mailServer"/>
            </xs:sequence>
            <xs:attribute name="mailDescription" use="required" type="xs:NCName"/>
            <xs:attribute name="mailName" use="required" type="xs:NCName"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="mailServer">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded" ref="pop3"/>
                <xs:element maxOccurs="unbounded" ref="user"/>
            </xs:sequence>
            <xs:attribute name="sp" use="required" type="xs:integer"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="pop3">
        <xs:complexType>
            <xs:attribute name="key" use="required" type="xs:NCName"/>
            <xs:attribute name="value" use="required" type="xs:NMTOKEN"/>
        </xs:complexType>
    </xs:element>
    <xs:element name="user">
        <xs:complexType>
            <xs:attribute name="key" use="required" type="xs:NCName"/>
            <xs:attribute name="value" use="required" type="xs:NCName"/>
        </xs:complexType>
    </xs:element>
</xs:schema>

对应的测试代码中这些写:

 File file = new File("D:\\Springboot\\AutoTest\\Chapter15\\src\\main\\resources\\XSD\\assertxsd.xsd");

        given()
                .proxy(8888)
                .when()
                .get("http://localhost:8889/getresponsewithfile")
                .then()
                .assertThat()
                .body(matchesXsd(file));//接口返回内容是xml,需要把xml转换成xml schema,然后生成一个文件,把文件传过来作为参数

DTD 例子

get("/videos").then().assertThat().body(matchesDtd(dtd));

moco的接口:

  {
    "description":"模拟返回内容是文件",
    "request" :
    {
      "uri":"/getresponsewithDTDfile",
      "method":"get"
    },
    "response" :
    {
      "file" : "D:/Springboot/AutoTest/Chapter15/src/main/resources/databaseConfig.xml"
    }
  }

接口返回的文件头中内容是:


image.png

我们需要把http://mybatis.org/dtd/mybatis-3-config.dtd 文件下到本地,在浏览器中输入该地址就能下载到文件
测试代码:

        File file1 = new File("D:\\Springboot\\AutoTest\\Chapter15\\src\\main\\resources\\XSD\\mybatis-3-config.dtd");
        given()
                .proxy(8888)
                .when()
                .get("http://localhost:8889/getresponsewithDTDfile")
                .then()
                .assertThat()
                .body(matchesDtd(file1));//需要传DTD的文件的地址

matchesXsdmatchesDtd方法在Hamcrest matchers里,你可以从io.restassured.matcher.RestAssuredMatchers
导入。

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

推荐阅读更多精彩内容