Pytest實戰API測試框架

Pytest實戰API測試框架

功能規劃

  1. 數據庫斷言 pymysql -> 封裝
  2. 環境清理 數據庫操作 -> Fixtures
  3. 并發執行 pytest-xdist 多進程并行
  4. 復合斷言 pytest-check
  5. 用例重跑 pytest-rerunfailures
  6. 環境切換 pytest-base-url
  7. 數據分離 pyyaml
  8. 配置分離 pytest.ini
  9. 報告生成 pytest-html, allure-pytest
  10. 用例等級 pytest-level
  11. 限制用例超時時間 pytest-timeout
  12. 發送報告郵件 通過自定Fixture及Hooks實現

安裝相應的包

pip安裝時可以通過-i https://pypi.doubanio.com/simple/,指定使用豆瓣的源, 下載稍微快一點

pip install requests 
pip install pymysql 
pip install pyyaml 
pip install pytest 
pip install pytest-xdist 
pip install pytest-check 
pip install pytest-rerunfailures 
pip intsall pytest-base-url 
pip intstall pytest-html 
pip install pytest-level
pip install  pytest-timeout

導出依賴到requirements.txt中

pip freeze > requirments.txt

結構規劃

分層結構

分層設計模式: 每一層為上層提供服務

用例層(測試用例)
  |
Fixtures輔助層(全局的數據、數據庫操作對象和業務流等)
  |
utils實用方法層(數據庫操作, 數據文件操作,發送郵件方法等等)

靜態目錄

  • data: 存放數據
  • reports: 存放報告

目錄結構

longteng17/
  - data/
    - data.yaml: 數據文件
  - reports/: 報告目錄
  - test_cases/: 用例目錄
    - pytest.ini:  pytest配置
    - api_test/:  接口用例目錄
      - conftest.py:  集中管理Fixtures方法
    - web_test/:  web用例目錄
    - app_test/:  app用例目錄
  - utils/: 輔助方法
    - data.py: 封裝讀取數據文件方法
    - db.py: 封裝數據庫操作方法
    - api.py: 封裝api請求方法
    - notify.py: 封裝發送郵件等通知方法
  - conftest.py: 用來放置通用的Fixtures和Hooks方法
  - pytest.ini: Pytest運行配置

規劃conftest.py的位置,要確保項目跟目錄被導入到環境變量路徑(sys.path)中去。
conftest.py及用例的導入機制為:

  1. 如果在包(同級有init.py)內,則導入最上層包(最外一個包含init.py)的父目錄。
  2. 如果所在目錄沒有init.py,直接導入conftest.py父目錄。

數據文件的選擇

  • 無結構
    • txt: 分行, 無結構的文本數據
  • 表格型
    • csv: 表格型, 適合大量同一類型的數據
    • excel: 表格型, 構造數據方便, 文件較大,解析較慢
  • 樹形
    • json: 可以存儲多層數據, 格式嚴格,不支持備注
    • yaml: 兼容json, 靈活,可以存儲多層數據
    • xml: 可以存儲多層, 文件格式教繁瑣
  • 配置型
    • .ini/.properties/.conf: 只能存儲1-2層數據, 適合配置文件

由于用例數據常常需要多層級的數據結構,這里選擇yaml文件作為本項目的數據文件,示例格式如下:

test_case1: 
    a: 1
    b: 2

數據第一層以用例名標識某條用例所使用的數據,這里約定要和用例名稱完全一致,方便后面使用Fixture方法自動向用例分配數據。

標記規劃

標記: mark, 也稱作標簽, 用來跨目錄分類用例方便靈活的選擇執行。

  • 按類型: api, web, app
  • 標記有bug: bug
  • 標記異常流程: negative

也可以根據自己的需求,按模塊、按是否有破壞性等來標記用例。

utils實用方法層

數據文件操作: data.py

首先需要安裝pyyaml, 安裝方法:pip install pyyaml
讀取yaml文件數據的方法為:

  1. 打開文件 with open(..) as f:
  2. 加載數據 data=yaml.safe_load(f)

yaml.safe_load()和yaml.load()的區別:

由于yaml文件也支持任意的Python對象
從文件中直接加載注入Python是極不安全的, safe_load()會屏蔽Python對象類型,只解析加載字典/列表/字符串/數字等級別類型數據

示例如下:

import yaml

def load_yaml_data(file_path):
    with open(file_path, encoding='utf-8') as f:
        data = yaml.safe_load(f)
    print("加載yaml文件: {file_path} 數據為: {data}")
    return data

為了示例簡單,這里沒有對文件不存在、文件格式非yaml等異常做處理。異常處理統一放到Fixture層進行。

假如項目要支持多種數據文件, 可以使用類來處理。

數據庫操作: db.py

這里使用pymysql, 安裝方法pip install pymysql

敏感數據處理

數據庫配置分離

數據庫密碼等敏感數據,直接放在代碼或配置文件中,會有暴露風險,用戶敏感數據我們可以放到環境變量中,然后從環境變量中讀取出來。

注意:部署項目時,應記得在服務器上配置相應的環境變量,才能運行。

Windows在環境變量中添加變量MYSQL_PWD,值為相應用戶的數據庫密碼,也可以將數據庫地址,用戶等信息也配置到環境變量中。
Linux/Mac用戶可以通過在/.bashrc或/.bash_profile或/etc/profile中添加

export MYSQL_PWD=數據庫密碼

然后source相應的文件使之生效,如source ~/.bashrc

Python中使用os.getenv('MYSQL_PWD')便可以拿到相應環境變量的值。

注意:如果使用PyCharm,設置完環境變量后,要重啟PyCharm才能讀取到新的環境變量值。

我們使用字典來存儲整個數據庫的配置,然后通過字典拆包傳遞給數據庫連接方法。

import os
import pymysql

DB_CONF = {
    'host': '數據庫地址',
    'port': 3306,
    'user': 'test',
    'password': os.getenv('MYSQL_PWD'),
    'db': 'longtengserver',
    'charset': 'utf8'
}

conn = pymysql.connect(**DB_CONF)

封裝數據庫操作方法

數據常見的操作方法有查詢,執行修改語句和關閉連接等。對應一種對象的多個方法,我們使用類來封裝。
同時為避免查詢語句和執行語句的串擾,我們在建立連接時使用autocommit=True來確保每條語句執行后都立即提交,完整代碼如下。

import os
import pymysql

DB_CONF = {
    'host': '數據庫地址',
    'port': 3306,
    'user': 'test',
    'password': os.getenv('MYSQL_PWD'),
    'db': 'longtengserver',
    'charset': 'utf8'
}

class DB(object):
    def __init__(self, db_conf=DB_CONF)
        self.conn = pymysql.connect(**db_conf, autocommit=True)
        self.cur = self.conn.cursor(pymysql.cursors.DictCursor)
        
    def query(self, sql):
        self.cur.execute(sql)
        data = self.cur.fetchall()
        print(f'查詢sql: {sql} 查詢結果: {data}')
        return data
        
    def change_db(self, sql):
        result = self.cur.execute(sql)
        print(f'執行sql: {sql} 影響行數: {result}')
        
    def close(self):
        print('關閉數據庫連接')
        self.cur.close()
        self.conn.close()

其中如果查詢中包含中文,要根據數據庫指定響應的charset,這里的charset值為utf8不能寫成utf-8。
self.conn.cursor(pymysql.cursors.DictCursor)這里使用了字典格式的游標,返回的查詢結果會包含響應的表字段名,結果更清晰。

由于所有sql語句都是單條自動提交,不支持事務,因此在change_db時,不需要再作事務異常回滾的操作,對于數據庫操作異常,統一在Fixture層簡單處理。

封裝常用數據庫操作

# db.py
...
class FuelCardDB(DB):
    def del_card(self, card_number):
        print(f'刪除加油卡: {card_number}')
        sql = f'DELETE FROM cardinfo WHERE cardNumber="{card_number}"'
        self.change_db(sql)

    def check_card(self, card_number):
        print(f'查詢加油卡: {card_number}')
        sql = f'SELECT id FROM cardinfo WHERE cardNumber="{card_number}"'
        res = self.query(sql)
        return True if res else False

    def add_card(self, card_number):
        print(f'添加加油卡: {card_number}')
        sql = f'INSERT INTO cardinfo (cardNumber) VALUES ({card_number})'
        self.change_db(sql)

發送郵件通知: notify.py

使用Python發送郵件

發送郵件一般要通過SMTP協議發送。首先要在你的郵箱設置中開啟SMTP服務,清楚SMTP服務器地址、端口號已經是否必須使用安全加密傳輸SSL等。
使用Python發送郵件分3步:

  1. 組裝郵件內容MIMEText
  2. 組裝郵件頭: From、To及Subject
  3. 登錄SMTP服務器發送郵件
  • 組裝郵件內容MIMEText
from email.mime.text import MIMEText
import smtplib
body = 'Hi, all\n附件中是測試報告, 如有問題請指出'
body2 = '<h2>測試報告</h2><p>以下為測試報告內容<p>'
# msg = MIMEText(content, 'plain', 'utf-8')
msg = MIMEText(content2, 'html', 'utf-8')

使用MIMEText組裝Email消息數據對象,正文支持純文本plain和html兩種格式。

  • 組裝郵件頭: From、To及Subject
...
msg['From'] = 'zhichao.han@qq.com'
msg['To'] = 'superhin@126.com'
msg['Subject'] = '接口測試報告'

msg['From']中也可以聲明收件人名稱,格式為:

msg['From'] = '<韓志超> zhichao.han@qq.com'

msg['To']中也可以寫多個收件人,寫到一個字符串中使用英文逗號隔開:

msg['To'] = 'superhin@126.com,ivan-me@163.com'

注意郵件頭的From、To只是一種聲明,并不一定是實際的發件人和收件人,比如From寫A郵箱,實際發送時,使用B郵箱的SMTP發送,便會形成代發郵件(B代表A發送)。

  • 登錄SMTP服務器發送郵件
...
smtp = smtplib.SMTP('郵箱SMTP地址')
# smtp = smtplib.SMTP_SSL('郵箱SMTP地址')
smtp.login('發件人郵箱', '密碼')
smtp.sendmail('發件人郵箱', '收件人郵箱', msg.as_string())

這里登錄SMTP和SMTP_SSL要看郵箱服務商支持哪種,連接時也可以指定端口號,如:

smtp = smtplib.SMTP_SSL('郵箱SMTP地址', 465)

登錄時的密碼根據郵箱的支持可以是授權碼或登錄密碼(一般如QQ郵箱采用授權碼,不支持使用登錄密碼登錄SMTP)。
sendmail發送郵件時,使用的發件人郵箱和收件人郵箱是實踐的發件人和收件人,可以和郵件頭中的不一致。但是發件人郵箱必須和登錄SMTP的郵箱一致。
sendmail每次只能給一個收件人發送郵件,當有多個收件人是,可以使用多次sendmail方法,示例如下:

receivers = ['superhin@163.com', 'zhichao.han@qq.com']
for person in receivers:
   smtp.sendmail('發件人郵箱', person, msg.as_string())

msg.as_string()是將msg消息對象序列化為字符串后發送。

發送帶附件的郵件

由于郵件正文會過濾掉大部分的樣式和JavaScript,因此直接將html報告讀取出來,放到郵件正文中往往沒有任何格式。這時,我們可以通過附件來發送測試報告。

郵件附件一般采用二進制流格式(application/octet-stream),正文則采用文本格式。要混合兩種格式我們需要使用MIMEMultipart這種混合的MIME格式,一般步驟為:

  1. 建立一個MIMEMultipart消息對象
  2. 添加MIMEText格式的正文
  3. 添加MIMEText格式的附件(打開附件,按Base64編碼轉為MIMEText格式)
  4. 添加郵件頭信息
  5. 發送郵件

示例代碼如下:

from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib

# 1. 建立一個MIMEMultipart消息對象
msg = MIMEMultipart()

# 2. 添加郵件正文
body = MIMEText('hi, all\n附件中是測試報告,請查收', 'plain', 'utf-8')
msg.attach(body)

# 3. 添加附件
att = MIMEText(open('report.html', 'rb').read(), 'base64', 'utf-8')
att['Content-Type'] = 'application/octet-stream'
att["Content-Disposition"] = 'attachment; filename=report.html'
msg.attach(att1)

# 4. 添加郵件頭信息
...

# 5. 發送郵件
...

使用消息對象msg的attach方法來添加MIMEText格式的郵件正文和附件。
構造附件MIMEText對象時,要使用rb模式打開文件,使用base64格式編碼,同時要聲明附件的內容類型Content-Type以及顯示排列Content-Dispositon,這里的attachment; filename=report.html,attachment代表附件圖標,filename代表顯示的文件名,這里表示圖標在左,文件名在右,顯示為report.html。

添加郵件頭信息和發送郵件同發送普通郵件一致。

發送郵件方法封裝

同樣,我們可以將敏感信息郵箱密碼配置到環境變量中去,這里變量名設置為SMTP_PWD。

import os
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import smtplib

SMTP_HOST = '郵箱SMTP地址'
SMTP_USER = '發件人郵箱'
SMTP_PWD = os.getenv('SMTP_PWD')

def send_email(self, body, subject, receivers, file_path):
    msg = MIMEMultipart()
    msg.attach(MIMEText(body, 'html', 'utf-8'))

    att1 = MIMEText(open(file_path, 'rb').read(), 'base64', 'utf-8')
    att1['Content-Type'] = 'application/octet-stream'
    att1["Content-Disposition"] = f'attachment; filename={file_name}'
    msg.attach(att1)
    
    msg['From'] = SMTP_USER
    msg['To'] = ','.join(receivers)
    msg['Subject'] = subject
    
    smtp = smtplib.SMTP_SSL(SMTP_HOST)
    smtp.login(SMTP_USER, SMTP_PWD)
    for person in receivers:
        print(f'發送郵件給: {person}')
        smtp.sendmail(SMTP_USER, person, msg.as_string())
    print('郵件發送成功')

同樣,為了示例簡單,這里并沒有對SMTP連接、登錄、發送郵件做異常處理,讀者可以進行相應的補充。

請求方法封裝:api.py

requests本身已經提供了很好的方法,特別是通用的請求方法requests.request()。這里的封裝只是簡單加了base_url組裝、默認的超時時間和打印信息。

import requests

TIMEOUT = 30

class Api(object):
    def __init__(self, base_url=None):
        self.session = requests.session()
        self.base_url = base_url

    def request(self, method, url, **kwargs):
        url = self.base_url + url if self.base_url else url
        kwargs['timeout'] = kwargs.get('timeout', TIMEOUT)
        res = self.session.request(method, url, **kwargs)
        print(f"發送請求: {method} {url} {kwargs} 響應數據: {res.text}")
        return res

    def get(self, url, **kwargs):
        return self.request('get', url, **kwargs)

    def post(self, url, **kwargs):
        return self.request('post', url, **kwargs)

這里,Api實例化時如果傳遞了base_url參數,則所有的url都會拼接上base_url。
kwargs['timeout'] = kwargs.get('timeout', TIMEOUT),設置默認的超時時間設置為30s。

Fixtures方法層

import pytest
from utils.data import Data
from utils.db import FuelCardDB
from utils.api import Api

@pytest.fixture(scope='session')
def data(request):
    basedir = request.config.rootdir
    try:
        data_file_path = os.path.join(basedir, 'data', 'api_data.yaml')
        data = Data().load_yaml(data_file_path)
    except Exception as ex:
        pytest.skip(str(ex))
    else:
        return data

@pytest.fixture(scope='session')
def db():
    try:
        db = FuelCardDB()
    except Exception as ex:
        pytest.skip(str(ex))
    else:
        yield db
        db.close()

@pytest.fixture(scope='session')
def api(base_url):
    api = Api(base_url)
    return api

這里對,utils實用方法層的異常進行簡單的skip處理,即當數據連接或數據文件有問題時,所有引用該Fixture的用例都會自動跳過。

在api這個Fixtures中我們引入了base_url,它來自于插件pytest-base-url,可以在運行時通過命令行選項--base-url或pytest.ini中的配置項base_url來指定。

[pytest]
...
base_url=http://....:8080

按用例名分發用例

Fixture方法通過用例參數,注入到用例中使用。Fixture方法中可以拿到用例所在的模塊,模塊變量,用例方法對象等數據,這些數據都封裝在Fixture方法的上下文參數request中。
原有的data這個Fixture方法為用例返回了數據文件中的所有數據,但是一般用例只需要當前用例的數據即可。我們在數據文件中第一層使用和用例方法名同名的項來區分各個用例的數據。如:

# api_data.yaml
test_add_fuel_card_normal: 
  data_source_id: bHRz
  cardNumber: hzc_00001
...

下面的示例演示了根據用例方法名分配數據的Fixture方法:

# conftest.py
...
@pytest.fixture
def case_data(request, data):
    case_name = request.function.__name__
    return data.get(case_name)

request是用例請求Fixture方法的上下文參數,里面包含了config對象、各種Pytest運行的上下文信息,可以通過Python的自省方法print(request.__dict__)查看request對象中所有的屬性。

  • request.function為調用Fixture的函數方法對象,如果是用例直接調用的Fixture,這里便是用例函數對象,通過函數對象的name屬性獲取到函數名。
  • 通過request.module拿到用例所在模塊,進而根據模塊中某些屬性作相應動態配置。
  • 通過request.config可以拿到pytest運行時的運行參數、配置參數值等信息。

這樣,用例中引入的case_data參數就只是該用例的數據。

用例層

一條完整的用例應包含以下步驟:

  1. 環境檢查或數據準備
  2. 業務操作
  3. 不止一條斷言語句(包括數據庫斷言)
  4. 環境清理

另外一般用例還應加上指定的標記。

import pytest

@pytest.mark.level(1)
@pytest.mark.api
def test_add_fuel_card_normal(api, db, case_data):
    """正常添加加油卡"""
    url = '/gasStation/process'
    data_source_id, card_number = case_data.get('data_source_id'), case_data.get('card_number')

    # 環境檢查
    if db.check_card(card_number):
        pytest.skip(f'卡號: {card_number} 已存在')

    json_data = {"dataSourceId": data_source_id, "methodId": "00A",
                 "CardInfo": {"cardNumber": card_number}}
    res_dict = api.post(url, json=json_data).json()

    # 響應斷言
    assert 200 == res_dict.get("code"))
    assert "添加卡成功" == res_dict.get("msg")
    assert res_dict.get('success') is True
    
    # 數據庫斷言
    assert db.check_card(card_number) is True

    # 環境清理
    db.del_card(card_number)

使用復合斷言:pytest-check

使用assert斷言時,當某一條斷言失敗后,該條用例即視為失敗,后面的斷言不會再進行判斷。有時我們需要每一次可以檢查所有的檢查點,輸出所有斷言失敗項。此時我們可以使用pytest-check插件進行復合斷言。
安裝方法pip install pytest-check。
所謂復合斷言即,當某條斷言失敗后仍繼續檢查下面的斷言,最后匯總所有失敗項。
pytest-check使用方法

import pytest_check as check
...
check.equal(200, es_dict.get("code"))
check.equal("添加卡成功",res_dict.get("msg"))
check.is_true(res_dict.get('success'))
check.is_true(db.check_card(card_number))

除此外常用的還有:

  • check.is_false():斷言值為False
  • check.is_none(): 斷言值為None
  • check.is_not_none():斷言值不為None

標記用例跳過和預期失敗

如果某些用例暫時環境不滿足無法運行可以標記為skip, 也可以使用skipif()判斷條件跳過。 對于已知Bug,尚未完成的功能也可以標記為xfail(預期失敗)。
使用方法如下:

import os
import pytest

@pytest.mark.skip(reason="暫時無法運行該用例")
def test_a():
    pass
    
@pytest.mark.skipif(os.getenv("MYSQL_PWD") is None, reason="缺失環境變量MYSQL_PWD配置")
def test_b():
    pass
    
@pytest.mark.xfail(reason='尚未解決的已知Bug')
def test_c():
    pass

test_b首先對環境變量做了檢查,如果沒有配置MYSQL_PWD這個環境變量,則會跳過該用例。
test_c為期望失敗,這時如果用例正常通過則視為異常的xpass狀態,失敗則為視為正常的xfail狀態,在--strict嚴格模式下,xfail視為用例通過,xpass視為用例失敗。
這里標記運行時分別使用-r/-x/-X顯示skip、xfail、xpass的原因說明:

pytest -rsxX

這里的-s可以在命令行上顯示用例中print的一些信息。

另外,也可以在Fixture方法或用例中,使用pytest.skip("跳過原因"), pytest.xfail("期望失敗原因")來根據條件表用例跳過和期望失敗。

標記skip和xfail屬于一種臨時隔離策略,等問題修復后,應及時去掉該標記。

運行控制

切換環境

運行時通過傳入--base-url來切換環境:

pytest --base-url=http://服務地址:端口號

失敗用例重跑

默認Pytest支持-lf參數來重跑上次失敗的用例。但如果我們想要本次用例失敗后自動重跑的話,可以使用pytest-rerunfailures插件。
安裝方法pip install pytest-rerunfailures。
運行時使用

pytest --reruns 3 --reruns-delay 1

來指定失敗用例延遲1s后自動重跑,最多重跑3次。

對應已知的不穩定用例,我們可以通過flasky標記,來使之失敗時自動重跑,示例如下。

import pytest
@pytest.mark.flaky(reruns=3, reruns_delay=1)
def test_example():
    import random
    assert random.choice([True, False])

按用例等級運行

使用pytest-level可以對用例標記等級,安裝方法: pip install pyest-level
使用方法:

@pytest.mark.level(1)
def test_basic_math():
    assert 1 + 1 == 2

@pytest.mark.level(2)
def test_intermediate_math():
    assert 10 / 2 == 5

@pytest.mark.level(3)
def test_complicated_math():
    assert 10 ** 3 == 1000

運行時通過--level來選擇運行的等級。

pytest --level 2

以上只會運行level1和level2的用例(數字越大,優先級約低)

限制用例執行時間

使用插件pytest-timeout可以限制用例的最大運行時間。
安裝方法:pip install pytest-timeout
使用方式為

pytest --timeout=30

或配置到pytest.ini中

...
timeout=30

用例并行

使用pytest-xdist可以開啟多個進程運行用例。
安裝方法:pip install pytest-xdist
使用方式

pytest -n 4

將所有用例分配到4個進程運行。

完整的項目配置文件

pytest.ini

[pytest]
miniversion = 5.0.0

addopts = --strict --html=report_{}.html --self-contained-html
base_url = http://115.28.108.130:8080
testpaths = test_cases/
markers =
    api: api test case
    web: web test case
    app: app test case
    negative: abnormal test case

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.

log_cli = true
log_cli_level = info
log_cli_format = %(asctime)s %(levelname)s %(message)s
log_cli_date_format = %Y-%m-%d %H:%M:%S

timeout = 10
timeout_func_only = true

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,565評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,115評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,577評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,514評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,234評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,621評論 1 326
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,641評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,822評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,380評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,128評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,319評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,879評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,548評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,970評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,229評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,048評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,285評論 2 376

推薦閱讀更多精彩內容

  • 功能規劃 數據庫斷言 pymysql -> 封裝 環境清理 數據庫操作 -> Fixtures 并發執行 pyte...
    琉璃_233a閱讀 319評論 0 0
  • 16-03-08 星期二 晴 62天 伙伴 晨間,送孩子去上學。 在學校門口,看見平日里跟她玩的最好,最瘋...
    年念玲閱讀 131評論 0 0
  • 蘇州游記 江淮夏日徒悶熱 舊景姑蘇夜方涼 一街燈火映石岸 幾處漁舟隱荷塘 拙園竹翠迷路徑 寒山寺遠無鐘聲 又慕范公...
    周洋_a972閱讀 139評論 0 0
  • 大伯今年60歲, 地地道道的農村人,生性樂觀,待人熱情,喜歡湊熱鬧. 閑時喜歡打點小麻將. 平時除了務農,偶爾在附...
    賢讀一書閱讀 206評論 0 0
  • 你翻過冬季是為了什么呢? 你打破黑夜是為了什么呢? 如果愛她 快 把情話說給她聽 趁黃昏降臨之前 趁她把耳朵閉起來...
    許家小米兒閱讀 249評論 0 1