Screeps 使用 Jest 添加單元測試

screeps 系列教程

前言

上篇文章 里,我們給自己的 screeps 項目引入了 typescript,這讓我們的代碼可靠性獲得了質的飛躍。那么為什么還要引入自動測試呢?問得好,我們先來想象一下下面的場景:

你花了好幾天完成了一個重要模塊,為了保證其可靠性,你進行了詳細而又認真的測試,測試完成后,你的模塊像一個精密的機械一樣,每行代碼都明確而可靠的運行著,你獲得了很大的成就感。

這個模塊穩定的運行了好久,直到有一天,你發現需要往這個模塊里添加一些代碼,添加完成后模塊依舊正常運行,所以你沒有在意,繼續去開發其他代碼了。突然有一天,代碼突然報了個錯,之后又恢復正常,你根據錯誤信息找到了對應的代碼,檢查了一遍之后發現,不對啊這段代碼沒改過不可能有問題啊。但是災難就此降臨,之后代碼偶爾就會報一個錯,由于沒辦法斷點調試,你開始往線上的代碼里插一堆 log,但是依舊分析不出問題究竟是什么,你也曾花大功夫在私服里進行詳細測試,但是問題依舊復現不出來。

你開始筋疲力盡胸口發堵,就像是一拳打在了棉花上。之前引以為傲的精密代碼現在就像是屎山一樣堆在那里,里邊到處插滿了 console.log 和調試代碼,像是一場進行不下去的手術。

是不是已經開始難受了,沒錯,這個問題同樣困擾著這個世界上的頂級開發者們,他們維護著比我們的 screeps 復雜的多的巨型項目。直到有一天,有人想到,如果我能把之前手工做的測試通過代碼的形式固化下來,以后修改代碼之后直接全部執行一遍,不就既省時又能讓代碼更可靠么,于是,自動化測試誕生了,這也就是我們今天要講的內容。

簡單介紹自動化測試

網上關于自動化測試的文章有很多,這里就簡單介紹一下 Screeps 相關的內容。

是騾子是馬拉出來溜溜,測試的本質就是這個。如果說 typescript 是靜態檢查,那么我們就可以把測試稱為動態檢查。通過真實的運行這段代碼并檢查其結果是否符合預期,由此來證明這段代碼是否可用。這就是進行測試的目的所在。

由于測試的內容很多,所以我們會把不同的內容分開寫,而每一段測試內容我們就稱為一個 測試用例。并且因為很多真實環境里用到的依賴我們在測試環境里并沒有,所以需要在測試之前“偽造”他們,讓被測試的代碼認為自己所處的環境就是真實環境。這個過程我們一般稱為 mock

當前業內已經出現了很多成熟的測試框架,而我們教程里使用的就是最近發展迅速的 jest。jest 由 facebook 維護,以零配置著稱,更多介紹詳見 jest 官方網站

Jest 是一個令人愉快的 JavaScript 測試框架,專注于簡潔明快。

jest 還是 mocha?

實際上,當前的 screeps 社區幾乎絕大多數項目使用的都是另一個老牌測試框架 mocha,包括我之前也在使用 mocha。那么為什么本文會介紹 jest 呢?

主要原因是 jest 所需的配置更少,適合新手入門。mocha 由于其靈活性,很多需要用到的工具都需要自行安裝,而 jest 已經內建了足夠好用的相應工具。并且這兩者的代碼風格都非常類似,你可以輕易的復用寫好的測試用例。網上也有很多這兩個框架的對比,這里不再贅述。

如果你想使用 mocha,沒有關系,直接百度 mocha ts 即可,或者參考我之前寫的 typescript 使用 mocha 進行單元測試。下文中除了涉及到 jest 的配置和用例寫法外,其他大部分都可以應用在引入了 mocha 的項目里。

在本系列教程里我們會著重介紹兩個測試方式,分別是 單元測試集成測試。單元測試是小,檢查每個函數每個功能是否正常,本文內容就是介紹如何使用單元測試。集成測試是大,通過運行整個腳本并記錄運行情況來檢查 bot 的整體可用性,將在下篇文章中介紹。

不過在深入介紹之前,我們按照慣例先來了解一下引入自動測試的優缺點,請根據自己的項目情況認真思考自己是否需要用到它。

單元測試優缺點對比

優點

  • 記錄模塊用法:每個測試用例都是被測試代碼的使用例子。并且這段代碼還可以執行,通過查閱測試用例,你可以很輕易的了解到這個模塊應該如何調用。

  • 更好的代碼質量:想要進行單元測試,就需要你的模塊解耦做的足夠好,不然測試起來會非常復雜。所以引入測試會迫使你過度耦合的模塊進行解耦,將職責不唯一的函數進行拆分,規范代碼中的副作用讓業務更清晰。通過對老項目進行大規模重構,你的代碼質量將更上一層樓。

  • 防止 bug 回歸:由于修復新 bug 導致原來的 bug 復現了,我們通常將其稱為 回歸,并由此誕生了回歸測試。而一旦測試用例寫好了,那在之后的測試中它都會被執行,如果有 bug 回歸了,那測試用例就必然會失敗,由此我們可以非常快速的發現回歸問題。

  • 方便測試極端場景:在游戲里有很多極端場景是很難復現的,例如一個 creep 會在特定地形、特地房間、有特定建筑、自己在執行特定任務時才會出現問題。而在測試用例里,代碼的執行環境完全是我們創建出來的,所以我們可以輕易的模擬出一個穩定的極端場景。

  • 支持斷點調試:沒錯,測試的終極,由于我們的測試是在本地而不是游戲服務器上進行的,所以我們終于可以逐行的執行代碼并查看其運行情況。斷點調試對于測試的重要性想必不須我多言。

缺點

  • 增加開發工作:俗話說,百行代碼,千行測試。想要得到一個完整測試的模塊,你需要寫非常多的測試用例,從正常輸入到異常輸入,從大數據量壓測到極端場景測試,這些測試代碼都需要你來完成。

  • 需要 mock 工具:還記得我們在游戲中使用的 Creep、Room、Game、Memory 這些習以為常的變量么,這些在測試環境里都沒有,你需要手動 mock 他們,這對于你的編碼功底和對游戲的了解程度是一個不小的考驗。不過下文我們會介紹如何進行 mock

  • mock 的不真實性:測試環境就算我們模擬的再真實,它也不是真正的運行環境。有些問題是因為 mock 偽造的不夠像導致的,并不能說明你的代碼真的有問題。

  • 問題永遠出現在你想不到的地方:你寫了很多的測試用例,那也只能說明針對這些使用場景,你的代碼不會出現問題。就算引入了自動測試,也并不代表著你的代碼就一定是絕對穩定的。

當然,如果你是抱著“可以不用,不能沒有”的想法來的,那么直接開始即可。引入自動測試不會帶來任何改變,甚至你不需要對項目進行任何改造,測試用例完全獨立于原先的游戲代碼,哪怕你一個測試用例都不寫也不會影響什么。

jest 安裝與配置

廢話說了這么多,終于可以開始寫碼了。本項目基于 Screeps 使用 TypeScript 進行靜態類型檢查 文中搭建的項目繼續完善,請確保你至少讀過這篇文章。

首先我們在項目中執行如下命令來安裝依賴:

npm install --save-dev jest ts-jest @types/jest @screeps/common

安裝完成后在根目錄下新增 jest.config.js 并填入如下內容:

const { pathsToModuleNameMapper } = require('ts-jest/utils')
const { compilerOptions } = require('./tsconfig')

module.exports = {
    preset: 'ts-jest',
    roots: ['<rootDir>'],
    transform: {
        '^.+\\.tsx?$': 'ts-jest'
    },
    moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths || {}, { prefix: '<rootDir>/' }),
    moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
}

配置好了 jest 我們再去 package.json 里新增測試命令,你的 scripts 里可能已經有了一個 test 命令,直接刪掉即可:

{
  "scripts": {
    "test": "jest",
    "test-c": "jest --coverage"
  }
}

OK,至此我們的配置就結束了,接下來就可以進行測試了,想要測試我們得先有一個被測試的東西,首先在 src/main.ts 里寫一個如下函數:

/**
 * 接受兩個數字并相加
 */
export const testFn = function (num1: number, num2: number): number {
    return num1 + num2
}  

很簡單對吧,接下來我們就用 Jest 對其進行測試,在同目錄下新建文件 main.test.ts 并填入如下內容:

import { testFn } from './main'

it('可以正常相加', () => {
    const result = testFn(1, 2)
    expect(result).toBe(3)
})

然后執行測試命令 npm run test 即可進行測試,很快控制臺中就會輸出測試結果:

測試通過

至此,我們的 jest 測試框架就引入成功了,接下來我們就來從剛才寫的 main.test.ts 文件開始,了解下什么叫做單元測試。

單元測試:代碼可用性的保障

單元測試(unit testing,簡稱單測),是指對軟件中的最小單元進行檢查和驗證。我們剛才寫的就是一個單測用例。注意這里的最小單元并不是指函數,哪怕一個類很復雜,如果他沒有對外部暴露其他細節,那他本身就是一個“單元”,所以這個概念與代碼量的多少無關。

在單元測試之上,還有功能測試,模塊測試乃至集成測試。越往后,每個用例所測試的范圍也就更大,同時更注重其他方面而不是細節。而作為測試的基石,單元測試的數量和質量就直接決定了你代碼是否可靠。

光說可能不太直觀,讓我們回到剛才寫的測試代碼:

import { testFn } from './main'

// 這個 it 就代表了一個測試用例
it('可以正常相加', () => {
    // 執行測試
    const result = testFn(1, 2)
    // 比較測試結果和我們的期望
    expect(result).toBe(3)
})

可以看到我們調用了一個 it 方法,每個 it 方法就是一個測試用例,他接受兩個參數,第一個參數是用例的介紹,第二個參數是一個函數,包含實際的測試代碼。一個文件里可以包含多個 it 方法調用。而這個文件就被稱為一個測試套件(suit)。在測試時,jest 會自動去尋找項目中所有以 .test.ts 結尾的文件,并將其作為測試文件執行。

可以看到我們并沒有引入 it 方法,因為 Jest 會自動的將測試需要的工具函數都注入到全局變量 global 上,所以我們可以直接調用。

幾乎每個測試用例都由三部分構成:構建測試素材、執行測試、檢查期望:

  • 構建測試素材:由于我們測試的這個函數太簡單了,所以不需要構建什么素材,對于復雜一些的測試,例如一個函數的參數是 creep 和一個 source。我們就需要先 mock 出來這些對象,然后再執行測試。
  • 執行測試:執行測試不必多說,就是正常的代碼調用。
  • 檢查期望:最后我們使用了一個 expect 函數,它也是 jest 的一個全局對象,我們就是使用它進行的期望檢查。expect(result).toBe(3) 這句話的意思就是 result 這個變量的值應該等于 3。關于 expect 的詳細文檔見 jest 官方文檔 - expect

實際上,expect 這類工具函數被統稱為斷言庫。它做的事情很簡單,讓我們可以更語義化的描述我們的期望,如果不符合期望的話它就會報錯。并且,除了報錯后它還會詳情的描述你究竟錯到那里了,例如我們把上面 toBe 里的 3 改成 100 再運行測試,就可以看到如下輸出:

測試未通過

可以看到,除了指出了哪里報錯,代碼還給出了期望值(Expected)和收到的實際值(Received),由此,我們就可以更直觀的了解到究竟錯在了哪里。

斷點調試

其實現在我們就可以借助 IDE 的能力對代碼進行斷點調試了,以 vscode 為例,我們在代碼里插入 debugger 關鍵字,然后 點擊 test npm 腳本后的調試按鈕 即可進入調試模式。當進程執行到 debbuger 后就會暫停代碼運行并啟動斷點調試,如下:

我的測試文件應該寫在哪里?

你可以選擇新建 test/unit 目錄,然后把所有的測試用例都寫在這里。又或者分開寫在 src/ 目錄下的對應模塊里,相比起來我更推薦后者,因為 screeps 里有可能會包含很多個相互獨立的模塊,把測試文件寫在對應的文件夾里可以提高模塊的內聚性。不過無論你用哪種方法,請記得測試文件的名字應該與被測試文件保持一致。

使用 jest 測試 screeps 代碼

上面我們了解了 jest 的基本使用,接下來就來介紹一下如何用 jest 測試我們的 screeps 代碼,這一部分最主要的內容,就是 screeps 環境的 mock。

上面我們曾經提到過,由于測試用例是在我們本地執行的,所以默認情況下測試環境就是一個純粹的 Node 環境(加上一點 jest 的全局注入)。而 screeps 環境里是有不少全局變量的,所以我們要先將其偽造出來,防止我們的 screeps 代碼因為找不到需要的對象而報錯。

首先我們來整理一下最基本的幾個 screeps 全局依賴:

  • Game:這么沒得說,肯定是要有的。
  • Memory:數據儲存對象,也要有。
  • lodash:screeps 默認在全局引入了 lodash,所以我們也要添加進來。
  • 一大堆的全局常量:screeps 里的常量都在全局,沒什么好說的,加就完事了。

ok,接下來開始干活,首先找到你的全局類型定義的地方(比如 src/global.d.ts 之類的,沒有就直接創建一個),我們來聲明一下接下來要設置的幾個全局變量:

declare module NodeJS {
    interface Global {
        Game: Game
        Memory: Memory
        _: _.LoDashStatic
    }
}

如果不設置的話,ts 有可能會禁止你往全局寫入這些變量。接下來我們執行如下命令來安裝 lodash 工具庫:

npm install --save-dev lodash@3.10.1

這里指定了 --save-dev,因為我們只需要在本地的測試環境使用它。之后我們會把它添加到 global 里,現在來思考一個嚴峻的問題,那些全局常量怎么辦,那么多我總不能一個一個寫吧?

欸,不用擔心,還記得我們在一開始安裝的 @screeps/common 依賴么,這個庫也被用在 screeps 的官方私服中,其中就定義著我們需要的所有常量。我們只需要將其引入即可。咱們在 mock 目錄里新建一個 index.ts 并填入如下內容,全局常量的引入就在最后一行:

import * as _ from 'lodash'
import constants from './constant'

/**
 * 偽造的全局 Game 類
 */
export class GameMock {
    creeps = {}
    rooms = {}
    spawns = {}
    time = 1
}

/**
 * 偽造的全局 Memory 類
 */
export class MemoryMock {
    creeps = {}
    rooms = {}
}

/**
 * 包含任意鍵值對的類
 */
 type AnyClass = {
    new (): any;
    [key: string]: any
}

/**
 * util - 快捷生成游戲對象創建函數
 * 
 * @param MockClass 偽造的基礎游戲類
 * @returns 一個函數,可以指定要生成類的任意屬性
 */
export const getMock = function<T> (MockClass: AnyClass): (props?: Partial<T>) => T {
    return (props = {}) => Object.assign(new MockClass() as T, props)
}

/**
 * 創建一個偽造的 Game 實例
 */
export const getMockGame = getMock<Game>(GameMock)

/**
 * 創建一個偽造的 Memory 實例
 */
export const getMockMemory = getMock<Memory>(MemoryMock)

/**
 * 刷新游戲環境
 * 將 global 改造成類似游戲中的環境
 */
export const refreshGlobalMock = function () {
    global.Game = getMockGame()
    global.Memory = getMockMemory()
    global._ = _
    // 下面的 @screeps/common/lib/constants 就是所有的全局常量
    Object.assign(global, require("@screeps/common/lib/constants"))
}

為了方便介紹,我把這些代碼都放在了同一個文件里,你可以根據自己需要把上面的代碼拆分到不同文件。

這段代碼里比較復雜的有兩個地方,一是 getMock 函數,這個咱們待會再講,二是末尾的 refreshGlobalMock 函數,這個就是我們 screeps 環境 mock 的入口,只需要調用這個函數,代碼執行環境就可以被我們改造成近似于 screeps 的樣子。

事實上,screeps 的全局變量遠不止這些,很多對象的原型類,比如 Creep、Room 也都被掛載在 global 上,不過我并不推薦你先 mock 整個 screeps 然后再開始寫測試用例,相反,我推薦 先 mock 一個基本的環境,然后根據你測試用例的依賴,一步步增加你的 mock 工具。


ok,現在我們已經完成了環境偽造函數的準備工作,那么怎么調用它呢?首先,我們 打開 jest.config.js,然后在 module.exports 導出的對象中填寫如下字段

module.exports = {
    // ...
    // 當 jest 環境準備好后執行的代碼文件
    setupFilesAfterEnv : [
        '<rootDir>/test/setup.ts'
    ],
    // ...
}

之后,我們在對應的 test 文件夾中新建一個 setup.ts 文件,并填入如下內容即可:

import { refreshGlobalMock } from './mock'

// 先進行環境 mock
refreshGlobalMock()
// 然后在每次測試用例執行前重置 mock 環境
beforeEach(refreshGlobalMock)

這里邊的 beforeEach 是什么呢?它也是 jest 注入的全局變量之一,作用是 在每個測試用例調用前執行傳入的函數。也就是說,我們每個測試用例執行前都會運行一遍 refreshGlobalMock,這樣不僅可以偽造 screeps 環境,也防止了上個測試用例污染了全局環境。

現在我們的 screeps 環境偽造就已經基本完成了,接下來就可以回到 src/main.test.ts 中測試一下了:

it('可以正常相加', () => { /** ... */ })

it('全局環境測試', () => {
    // 全局應定義了 Game
    expect(Game).toBeDefined()
    // 全局應定義了 lodash
    expect(_).toBeDefined()
    // 全局的 Memory 應該定義且包含基礎的字段
    expect(Memory).toMatchObject({ rooms: {}, creeps: {} })
})

執行后即可看到測試通過,如果你沒有通過測試的話請根據報錯提示查找原因。


偽造好了測試環境后,現在我們回頭講一下 test/mock/index.ts 中出現的 getMock 函數,它實際上是一個工具函數,用于快速創建 mock 的實例(的生成函數),在上面我們已經用其生成了 getGameMock 和 getMemoryMock,除此之外,我們也可以用它來生成其他的游戲對象,例如最為常用的 creep:

test/mock/Creep.ts

import { getMock } from './index'

// 偽造 creep 的默認值
class CreepMock {
    body: BodyPartDefinition[] = [{ type: MOVE, hits: 100 }]
    fatigue: number = 0
    hits: number = 100
    hitsMax: number = 100
    id: Id<this> = `${new Date().getTime()}${Math.random()}` as Id<this>
    memory: CreepMemory = { role: 'harvester' , working: false }
    my: boolean = true
    name: string = `creep${this.id}`
    owner: Owner = { username: 'hopgoldy' }
    room: Room
    spawning: boolean = false
    saying: string
    store: StoreDefinition
    ticksToLive: number | undefined = 1500
}

/**
 * 偽造一個 creep
 * @param props 該 creep 的屬性
 */
export const getMockCreep = getMock<Creep>(CreepMock)

然后我們就可以在測試用例里使用 getMockCreep 來創建我們需要的 creep 實例:

// 需要提前在 tsconfig.json 的 paths 中配置 "@mock/*": ["./test/mock/*"]
import { getMockCreep } from '@mock/Creep'

it('mock Creep 可以正常使用', () => {
    // 創建一個 creep 并指定其屬性
    const creep = getMockCreep({ name: 'test', ticksToLive: 100 })

    expect(creep.name).toBe('test')
    expect(creep.ticksToLive).toBe(100)
})

可以看到,我們可以通過 getMockCreep 創建一個類型為 Creep,并且還擁有我們自定義屬性的 creep 實例,我們可以通過給 getMockCreep 傳入 creep 原型上存在的任意屬性(包括方法)來進行自定義。

接下來我們來學習一個可以讓測試更加方便的 mock 小工具,它同樣被集成到了 jest 中。

Jest mock 函數

假如我們有一個函數,它接受一個 creep 和一個 source 作為參數,當 source 的容量大于 0 時,就會調用 creep 的 harvest 方法,那么怎么檢查它調用了幾次呢。你可能會想到給 harvest 方法賦值一個函數并閉包保存一個值,當函數調用時進行累加。

這種方法也可以,不過還有種更簡單的方法,那就是我們接下來要介紹的 mock function:它可以記錄自己被調用的次數、被調用時接受的參數等等,在測試領域這類函數被稱為 spy。在 jest 中我們可以通過 jest.fn() 創建一個 mock function,如下:

/**
 * 當 source 里有能量時讓 creep 執行采集
 */
const useHarvest = function (creep: Creep, source: Source): void {
    if (source.energy > 0) creep.harvest(source)
}

it('useHarvest 可以正確調用 harvest 方法', () => {
    const mockHarvest = jest.fn()
    // 構建測試素材
    const creep = getMockCreep({ harvest: mockHarvest })
    const hasEnergySource = { energy: 100 } as Source
    const noEnergySource = { energy: 0 } as Source

    // 執行測試
    useHarvest(creep, hasEnergySource)
    useHarvest(creep, hasEnergySource)
    useHarvest(creep, noEnergySource)

    // 檢查期望
    expect(mockHarvest).toBeCalledTimes(2)
    // 這兩種寫法結果相同
    expect(mockHarvest.mock.calls).toHaveLength(2)

    console.log(mockHarvest.mock.calls)
    // > [ [ { energy: 100 } ], [ { energy: 100 } ] ]
})

可以看到,由于 mockHarvest 可以記錄調用內容,所以我們可以很輕易的進行判斷。并且被調用的參數也會被保存到 mockHarvest.mock.calls 中,你也可以用它來檢查具體的傳入參數是否正確。更多相關文檔可以參閱 jest 官方文檔 mock-function

單元測試覆蓋率

教程的最后,我們來介紹一下什么是單測覆蓋率,我們可以用一些工具監聽測試用例的執行,并分析我們的代碼,由此來展示測試用例“覆蓋”了哪些邏輯。我們可以簡單的認為這個指標越高,代碼就越可靠。

在 jest 中已經集成了覆蓋率檢查工具 istanbul。并且我們剛開始配置時已經新增了測試命令,所以我們直接執行如下命令即可查看單測覆蓋率:

npm run test-c

執行結果如下:

一堆綠啊,很好看,不過由于測試覆蓋率只會檢查測試用例涉及到的函數,所以這里的測試結果其實并不怎么準確。所以接下來我會以之前開發的 screeps 漢化補丁 為例來進行講解,下面是其覆蓋率報告:

screeps-chinese-pack 的單測覆蓋率報告

其中的分列含義如下:

  • Stmts:語句覆蓋率,是不是每個語句都執行了
  • Btanch:分支覆蓋率,由分支語句如 if-else 產生的分支覆蓋了多少
  • Funcs:函數覆蓋率,測試覆蓋了多少函數
  • Lines:行覆蓋率,測試覆蓋了本文件多少行
  • Uncovered Line:哪些主要代碼行沒有覆蓋到
  • Path:路徑覆蓋率,這個報告中并沒有包含,路徑覆蓋率是分支覆蓋率的升級版,例如三個同級的 if-else 會產生 8 中不同的路徑分支,這也是對代碼覆蓋率的終極體現。

主要的衡量指標就是 all files 的語句覆蓋率,一般認為應至少達到 80%,越高越好。

不僅如此,我們還可以項目根目錄的 coverage 目錄中找到它生成的詳細覆蓋率報告,我們可以直接在瀏覽器中打開 coverage\lcov-report\index.html 文件,就可以看到哪些內容被覆蓋到,非常直觀,這里不再贅述。

總結

恭喜你看完了這篇超長教程,本篇文章介紹了如何在 screeps 項目中引入單測和如何書寫單測用例,之后對 screeps 環境進行了基本模擬以及簡單介紹了一下單測覆蓋率。要記住,我們現在有測試環境和 screeps 游戲環境兩套環境,在 screeps 環境中(src 目錄下的代碼)我們只能使用游戲提供的 api。而在測試環境中,我們可以使用完整的 node 能力進行開發。牢記這一點并注意代碼是運行在哪個環境里的,別讓 screeps 局限了你的想象力。

現在你就可以好好審視一下自己的項目,然后開始自己的測試之旅吧!

想要查看更多教程?歡迎訪問 《Screeps 中文教程》或者訪問 《Screeps 搭建開發環境 - 導言》 來繼續升級你的項目!

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

推薦閱讀更多精彩內容