如何使用Page Object 设计模型

什么是 PageObject 设计模型?

PageObject 设计模型是在自动化测试过程中普遍采用的一种设计模式。它通过对页面对象(实际的 UI 页面,或者是逻辑上的页面)进行抽象,使得你的代码能在页面元素发生改变时,尽量少地更改,以最大程度地支持代码重用和避免代码冗余。页面对象模型的核心理念是,以页面(Web Page 或者 Native App Page)为单位来封装页面上的控件以及控件的部分操作。而测试用例,更确切地说是操作函数,基于页面封装对象来完成具体的界面操作,最典型的模式是“XXXPage.YYYComponent.ZZZOperation”。


PageObject 设计模型的特征

目前,并没有一种统一的格式(format)来告诉你如何设计 PageObject,只要你设计的代码将页面元素和测试代码分离,你都可以说你使用了 PageObject。

一般情况下,实现了 PageObject 的代码往往具备如下特征:

1.页面封装成 Page 类,页面元素为 Page 类的成员元素,页面功能放在 Page 类方法里。

将一个页面(或者待测试对象)封装成一个类(Class),把它称作 Page 类。Page 类里包括了这个页面(或者待测试对象)上的所有的元素,以及针对页面元素的操作方法(单步操作或者多步操作,一般会定义类方法)。注意:这个Page 类里仅仅包括当前页面,一般不包括针对其他页面的操作。

2.针对这个 Page 类定义一个测试类,在测试类调用 Page 类的各个类方法完成测试。

也就是测试代码和被测试页面的页面代码解耦,当页面本身发生变化,例如元素定位发生改变、页面布局改变后,仅需要更改相对应的 Page 类的代码,而无须更改测试类的代码。

PageObject 模式减少了代码冗余,可以使业务流程变得清晰易读,降低了测试代码维护成本。


PageObject 的实现

根据上述特点,我们来看下一个 PageObject 的经典设计:

可以看到,在测试类里,我们会定义许多测试方法,这些测试方法里,会含有对页面对象实例的调用;而页面对象实例,是通过页面对象类进行初始化操作生成的;对于许多页面对象类都存在的通用操作,我们会提取到页面对象基类里。

通过这种方法,我们就实现了:

一个页面元素在整个项目中,仅存在一处定义,其他都是调用;

Page 类通用的操作进一步提取到 BasePage 类,减少了代码冗余。


PageObject 的 Python 库

在 Python 里,有专门针对 PageObject 的 Python 库 Page Objects。使用 Page Objects 可以迅速实现 PageObject 模式,下面来看下 Page Objects 库的使用。

安装:

pip install page_objects

应用:

# 以下为官方示例

>>> from page_objects import PageObject, PageElement

>>> from selenium import webdriver

>>> class LoginPage(PageObject):

        username = PageElement(id_='username')

        password = PageElement(name='password')

        login = PageElement(css='input[type="submit"]')

>>> driver = webdriver.PhantomJS()

>>> driver.get("http://example.com")

>>> page = LoginPage(driver)

>>> page.username = 'secret'

>>> page.password = 'squirrel'

>>> assert page.username.text == 'secret'

>>> page.login.click()


项目实战 —— PageObject 应用

好,我们不仅了解了 PageObject 的理论,也了解了 page_objects 这个 Python 库的使用。

现在,我们给我们的项目应用 pageObject 模型。

第一步:改造项目结构

我们来按照 PageObject 的实现来改造我们的项目结构。改造前,我们的目录结构:

|--lagouAPITest

    |--tests

        |--test_ones.py

        |--__init__.py

    |--common

        |--__init__.py

其中,test_ones.py 是我上文章新建的文件,其余目录及 init.py 都是空目录、空文件。

使用 PageObject 改造后,我们期望的目录结构:

|--lagouAPITest

    |--pages

        |--ones.py

        |--base_page.py

    |--tests

        |--test_ones.py

        |--__init__.py

    |--common

        |--__init__.py

改造后,我们把原本的 tests 目录下的 test_ones.py 里关于页面元素的操作全部剥离到 pages 文件夹下的 ones.py 里,然后针对可能的公用的操作,我们进一步抽象到 base_page.py 里去。

下面先看下原来 tests 文件夹下 test_ones.py 的内容:

# -*- coding: utf-8 -*-

import json

import requests

import pytest

from selenium import webdriver

from selenium.webdriver.common.by import By

from selenium.webdriver.support.ui import WebDriverWait

from selenium.webdriver.support import expected_conditions as EC


def cookie_to_selenium_format(cookie):

    cookie_selenium_mapping = {'path': '', 'secure': '', 'name': '', 'value': '', 'expires': ''}

    cookie_dict = {}

    if getattr(cookie, 'domain_initial_dot'):

        cookie_dict['domain'] = '.' + getattr(cookie, 'domain')

    else:

        cookie_dict['domain'] = getattr(cookie, 'domain')

    for k in list(cookie_selenium_mapping.keys()):

        key = k

        value = getattr(cookie, k)

        cookie_dict[key] = value

    return cookie_dict


class TestOneAI:

    # 在pytest里,针对一个类方法的setup为setup_method,

    # setup_method作用同unittest里的setUp()

    def setup_method(self, method):

        self.s = requests.Session()

        self.login_url = 'https://ones.ai/project/api/project/auth/login'

        self.home_page = 'https://ones.ai/project/#/home/project'

        self.header = {

            "user-agent": "user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",

            "content-type": "application/json"}

        self.driver = webdriver.Chrome()

    @pytest.mark.parametrize('login_data, project_name', [({"password": "iTestingIsGood", "email": "pleasefollowiTesting@outlook.com"}, {"project_name":"VIPTEST"})])

    def test_merge_api_ui(self, login_data, project_name):

        result = self.s.post(self.login_url, data=json.dumps(login_data), headers=self.header)

        assert result.status_code == 200

        assert json.loads(result.text)["user"]["email"].lower() == login_data["email"]

        all_cookies = self.s.cookies._cookies[".ones.ai"]["/"]

        self.driver.get(self.home_page)

        self.driver.delete_all_cookies()

        for k, v in all_cookies.items():

            print(v)

            print(type(v))

            self.driver.add_cookie(cookie_to_selenium_format(v))

        self.driver.get(self.home_page)

        try:

            element = WebDriverWait(self.driver, 30).until(

                EC.presence_of_element_located((By.CSS_SELECTOR, '[class="company-title-text"]')))

            assert element.get_attribute("innerHTML") == project_name["project_name"]

        except TimeoutError:

            raise TimeoutError('Run time out')

    # 在pytest里,针对一个类方法的teardown为teardown_method,

    # teardown_method作用同unittest里的dearDown()

    def teardown_method(self, method):

        self.s.close()

        self.driver.quit()


第二步,创建 Page 类

首先把跟页面有关的全部操作都放到 Page 类里,pages/ones.py 的代码如下:

# -*- coding: utf-8 -*-

import json

import requests

from selenium import webdriver

from selenium.webdriver.common.by import By

from selenium.webdriver.support.ui import WebDriverWait

from selenium.webdriver.support import expected_conditions as EC

from page_objects import PageObject, PageElement


def cookie_to_selenium_format(cookie):

    cookie_selenium_mapping = {'path': '', 'secure': '', 'name': '', 'value': '', 'expires': ''}

    cookie_dict = {}

    if getattr(cookie, 'domain_initial_dot'):

        cookie_dict['domain'] = '.' + getattr(cookie, 'domain')

    else:

        cookie_dict['domain'] = getattr(cookie, 'domain')

    for k in list(cookie_selenium_mapping.keys()):

        key = k

        value = getattr(cookie, k)

        cookie_dict[key] = value

    return cookie_dict


class OneAI(PageObject):

    # 使用page_objects库把元素locator, 元素定位,元素操作分离

    # 元素定位的字符串

    PROJECT_NAME_LOCATOR = '[class="company-title-text"]'

    NEW_PROJECT_LOCATOR = '.ones-btn.ones-btn-primary'


    # 元素定位

    new_project = PageElement(css=NEW_PROJECT_LOCATOR)

    # 通过构造函数初始化浏览器driver,requests.Session()

    # 通过api_login方法直接带登录态到达待测试页面开始测试

    def __init__(self, login_credential, target_page):

        self.login_url = 'https://ones.ai/project/api/project/auth/login'

        self.header = {

            "user-agent": "user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",

            "content-type": "application/json"}

        self.s = requests.Session()

        self.driver = webdriver.Chrome()

        self.api_login(login_credential, target_page)

    # 融合API测试和UI测试,并传递登录态到浏览器Driver供使用

    def api_login(self, login_credential, target_page):

        target_url = json.loads(json.dumps(target_page))

        try:

            result = self.s.post(self.login_url, data=json.dumps(login_credential), headers=self.header)

            assert result.status_code == 200

            assert json.loads(result.text)["user"]["email"].lower() == login_credential["email"]

        except Exception:

            raise Exception("Login Failed, please check!")

        all_cookies = self.s.cookies._cookies[".ones.ai"]["/"]

        self.driver.get(target_url["target_page"])

        self.driver.delete_all_cookies()

        for k, v in all_cookies.items():

            self.driver.add_cookie(cookie_to_selenium_format(v))

        self.driver.get(target_url["target_page"])

        return self.driver

    # 功能函数

    def get_project_name(self):

        try:

            project_name = WebDriverWait(self.driver, 30).until(

                EC.presence_of_element_located((By.CSS_SELECTOR, self.PROJECT_NAME_LOCATOR)))

            return project_name.get_attribute("innerHTML")

        except TimeoutError:

            raise TimeoutError('Run time out')

我们来看下这个 ones.py 文件:

首先,我在其中定义了一个方法 cookie_to_selenium_format,这个方法是把通过 requests.Session() 拿到的 cookies 转换成 Selenium/WebDrvier 认可的格式,这个函数跟我们的 Page 类无关,所以我把它放在 Page 类外;

接着,我定义了 OneAI 这个 Page 类,并且按照 page_objects 这个库的推荐写法写了元素的定位。注意,我把元素的 Locator 本身,以及元素、元素操作都分离开了。这样当有任意一个修改时,都不影响另外两个;

然后我又写了类方法,一个是用于拿登录态直接通过浏览器访问页面的函数 api_login,还有一个就是获取 project_name 的文本的函数 get_project_name。

至此,我的第一版 Page 类就创建完毕,但注意我这个 Page 类里是不包括测试的方法的。


第三步, 更新 TestPage 类

Page 类创建好,我们就要创建 Page 类对应的测试类。更改 tests 文件夹下的 test_ones.py 文件,更改后的内容如下:

# -*- coding: utf-8 -*-

import pytest

from pages.ones import OneAI


class TestOneAI:

    # 注意,需要email和密码需要更改成你自己的账户密码

    @pytest.mark.parametrize('login_data, project_name, target_page', [({"password": "iTestingIsGood", "email": "pleasefollowiTesting@outlook.com"}, {"project_name":"VIPTEST"}, {"target_page": "https://ones.ai/project/#/home/project"})])

    def test_project_name_txt(self, login_data, project_name, target_page):

        print(login_data)

        one_page = OneAI(login_data, target_page)

        actual_project_name = one_page.get_project_name()

        assert actual_project_name == project_name["project_name"]      #断言

你可以看到,这个测试类就变得非常简洁。它只包括一个测试方法,即 test_project_name_txt。这个函数用来测试我们拿到的 project name 是不是等于我们提供的那个值,即 VIPTEST。

你需要注意,在测试类中,应该仅仅包括对 Page 类的各种方法的调用,而不能在测试类中直接去操作测试类对象生成新的功能。

我们在命令行运行下,输入以下命令:

D:\_Automation\lagouAPITest>pytest tests/test_ones.py

至此,Page 类和 TestPage 类的解耦已经完成。


第四步, 提炼 BasePage 类

至此,PageObject 模式我们已经应用到我们的项目中了,不过你发现没有,我们的 Page 类里还有很多可以优化的地方,比如 cookie_to_selenium_format 这个方法,它不属于某一个具体的 Page 类,但又可以被多个 Page 类调用。

再比如,初始化浏览器 Driver 的代码,和初始化 requests.Session() 的代码也不属于某一个具体的 Page,但是我们把它放入了 Page 里,所以,我们继续优化,在 pages 文件夹下创建 base_page 类,把跟 page 无关的操作都提炼出来。

pages/base_page.py 文件的内容如下:

# -*- coding: utf-8 -*-

import json

import requests

from selenium import webdriver

from page_objects import PageObject, PageElement


def cookie_to_selenium_format(cookie):

    cookie_selenium_mapping = {'path': '', 'secure': '', 'name': '', 'value': '', 'expires': ''}

    cookie_dict = {}

    if getattr(cookie, 'domain_initial_dot'):

        cookie_dict['domain'] = '.' + getattr(cookie, 'domain')

    else:

        cookie_dict['domain'] = getattr(cookie, 'domain')

    for k in list(cookie_selenium_mapping.keys()):

        key = k

        value = getattr(cookie, k)

        cookie_dict[key] = value

    return cookie_dict


class BasePage(PageObject):

    def __init__(self, login_credential, target_page):

        self.login_url = 'https://ones.ai/project/api/project/auth/login'

        self.header = {

            "user-agent": "user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",

            "content-type": "application/json"}

        self.s = requests.Session()

        self.driver = webdriver.Chrome()

        self._api_login(login_credential, target_page)

    def _api_login(self, login_credential, target_page):

        target_url = json.loads(json.dumps(target_page))

        try:

            result = self.s.post(self.login_url, data=json.dumps(login_credential), headers=self.header)

            assert result.status_code == 200

            assert json.loads(result.text)["user"]["email"].lower() == login_credential["email"]

        except Exception:

            raise Exception("Login Failed, please check!")

        all_cookies = self.s.cookies._cookies[".ones.ai"]["/"]

        self.driver.get(target_url["target_page"])

        self.driver.delete_all_cookies()

        for k, v in all_cookies.items():

            self.driver.add_cookie(cookie_to_selenium_format(v))

        self.driver.get(target_url["target_page"])

        return self.driver

在 BasePage 这个类里,我把跟具体的某一个 page 的操作都剔除掉,仅仅留下共用的部分,比如初始化浏览器 driver、初始化 requests.Session(),然后我把用于登录后传递登录态的方法 api_login 改成一个类保护方法_api_login(即只允许 BasePage 的类实例和它的子类实例能访问_api_login 方法)。

这个时候,我们的 pages 文件夹下的 ones.py 也要做相应更改,更新后的 pages/ones.py 的内容如下:

# -*- coding: utf-8 -*-

from selenium.webdriver.common.by import By

from selenium.webdriver.support.ui import WebDriverWait

from selenium.webdriver.support import expected_conditions as EC

from page_objects import PageObject, PageElement

from pages.base_page import BasePage


class OneAI(BasePage):

    PROJECT_NAME_LOCATOR = '[class="company-title-text"]'

    NEW_PROJECT_LOCATOR = '.ones-btn.ones-btn-primary'

    new_project = PageElement(css=NEW_PROJECT_LOCATOR)

    def __init__(self, login_credential, target_page):

        super().__init__(login_credential, target_page)

    def get_project_name(self):

        try:

            project_name = WebDriverWait(self.driver, 30).until(

                EC.presence_of_element_located((By.CSS_SELECTOR, self.PROJECT_NAME_LOCATOR)))

            return project_name.get_attribute("innerHTML")

        except TimeoutError:

            raise TimeoutError('Run time out')

可以看到。我们的 Page 类进一步简化,只包括 Page 本身的元素、对象和操作,而不包括其他的部分,比如对浏览器 Driver 的初始化、对 requests.Session() 的初始化、登录等操作了。


第五步, 打造通用性测试框架

好了,本节课到现在,我们已经把 PageObject 模式的应用全部掌握了。现在来看看我们的框架,你觉得还有改进的空间吗?

|--lagouAPITest

    |--pages

        |--ones.py

        |--base_page.py

    |--tests

        |--test_ones.py

        |--__init__.py

    |--common

        |--__init__.py

当然有改进空间了, 再看看 base_page.py 这个文件。既然是 base_page,那么只应该跟 page 有关系,可是我们把初始化浏览器 driver、初始化 requests.Session() 这样的操作也放进去了,是不是不太合理?还有万一以后的浏览器测试不用 Selenium/WebDriver 了呢?

我个人估计这个进程会很快,现在前端自动化这边 Cypress 发展迅猛,而且上手非常简单。一个人通过简单学习很快就能通过 Cypress 来搭建基于持续集成的自动化测试框架。关于 Cypress,你可以参考我的新书《前端自动化测试框架 Cypress 从入门到精通》。

那么假设以后我们的浏览器测试不用Selenium/webDriver了,还有,万一有比 requests 更好用的HTTP库了呢?所以,有必要进一步拆分。

于是,我们的框架结构就变成如下的样子:

|--lagouAPITest

    |--pages

        |--ones.py

        |--base_page.py

    |--tests

        |--test_ones.py

        |--__init__.py

    |--common

        |--__init__.py

        |--selenium_helper.py

        |--requests_helper.py

把 BasePage 这个类里的关于 Selenium/WebDriver 和 Requests 的操作分别拆分到 selenium_helper.py 里和 requests_helper.py 里去。

selenium_helper.py 的内容如下:

from selenium import webdriver

class SeleniumHelper(object):

    @staticmethod

    def initial_driver(browser_name='chrome'):

        browser_name = browser_name.lower()

        if browser_name not in {'chrome', 'firefox', 'ff', 'ie'}:

            browser_name = 'chrome'

        if browser_name == 'chrome':

            browser = webdriver.Chrome()

        elif browser_name in ('firefox', 'ff'):

            browser = webdriver.Firefox()

        elif browser_name == 'ie':

            webdriver.Ie()

        browser.maximize_window()

        browser.implicitly_wait(60)

        return browser

    @staticmethod

    def cookie_to_selenium_format(cookie):

        cookie_selenium_mapping = {'path': '', 'secure': '', 'name': '', 'value': '', 'expires': ''}

        cookie_dict = {}

        if getattr(cookie, 'domain_initial_dot'):

            cookie_dict['domain'] = '.' + getattr(cookie, 'domain')

        else:

            cookie_dict['domain'] = getattr(cookie, 'domain')

        for k in list(cookie_selenium_mapping.keys()):

            key = k

            value = getattr(cookie, k)

            cookie_dict[key] = value

        return cookie_dict

selenium_helper.py 里包括了所有针对 Selenium 的操作,以后针对浏览器的各种操作全部都放在这个文件。

requests_helper.py 里的代码,更新后如下:

import json

import traceback

import requests

from requests.packages.urllib3.exceptions import InsecureRequestWarning

# Disable https security warning

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)

class SharedAPI(object):

    def __init__(self):

        self.s = requests.session()

        self.login_url = 'https://ones.ai/project/api/project/auth/login'

        self.header = {

            "user-agent": "user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36",

            "content-type": "application/json"}

    def login(self, login_credential):

        try:

            result = self.s.post(self.login_url, data=json.dumps(login_credential), headers=self.header, verify=False)

            if int(result.status_code) == 200:

                pass

            else:

                raise Exception('login failed')

            return result

        except RuntimeError:

            traceback.print_exc()

    def post_api(self, url, **kwargs):

        return self.s.post(url, **kwargs)

    def get_api(self, url, **kwargs):

        return self.s.get(url, **kwargs)

requests_helper.py 里包括所有对 requests 这个库的操作。

最后,我们还需要更新下 base_page.py,base_page.py 更新后,内容如下:

复制代码

# -*- coding: utf-8 -*-

import json

import traceback

import requests

from selenium import webdriver

from page_objects import PageObject, PageElement

from common.requests_helper import SharedAPI

from common.selenium_helper import SeleniumHelper

class BasePage(PageObject):

    def __init__(self, login_credential, target_page):

        self.api_driver = SharedAPI()

        self.loginResult = self.api_driver.login(login_credential)

        self.driver = SeleniumHelper.initial_driver()

        self._api_login(login_credential, target_page)

    def _api_login(self, login_credential, target_page):

        target_url = json.loads(json.dumps(target_page))

        assert json.loads(self.loginResult.text)["user"]["email"].lower() == login_credential["email"]

        all_cookies = self.loginResult.cookies._cookies[".ones.ai"]["/"]

        self.driver.get(target_url["target_page"])

        self.driver.delete_all_cookies()

        for k, v in all_cookies.items():

            self.driver.add_cookie(SeleniumHelper.cookie_to_selenium_format(v))

        self.driver.get(target_url["target_page"])

        return self.driver

可以看到,在更新后的 base_page.py 里,我们初始化 requests.Session() 和浏览器的 Driver 的方式是通过调用 SharedAPI 和 SeleniumHelper 这两个类。然后 BasePage 这个类里现在只包括各个 Page 类可以共用的函数,而不再包括无关的操作。

其他文件无须更改。

通过如下命令运行:

D:\_Automation\lagouAPITest>pytest --alluredir=./allure_reports

然后使用命令行进入到你的项目根目录下,执行如下语句:

D:\_Automation\lagouAPITest>allure serve allure_reports

接着你的默认浏览器会自动打开测试报告,看下运行结果:

好的,大功告成。

你发现没有,通过这个方式,我们就把自动化测试框架进行了框架代码和业务代码的剥离。此后我们的框架不仅看起来结构清晰,而且也变得跟业务松耦合。当你需要在新项目应用自动化的时候,仅仅把 pages 和 tests 这两个文件夹更换,便能够一“秒”搭建新的测试框架。

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

推荐阅读更多精彩内容