Vue单元测试实战教程(Mocha/Karma + Vue-Test-Utils + Chai)

原文链接://www.greatytc.com/p/38a37d5fccb2

视屏链接:https://www.bilibili.com/video/av54531291

在《前端进阶之路: 前端架构设计(3) - 测试核心》这边文章中, 通过分析了"传统手工测试的局限性" 去引出了测试驱动开发的理念, 并介绍了一些测试工具. 这篇文章我将通过一个Vue的项目, 去讲解如何使用mocha & karma, 且结合vue官方推荐的vue-test-utils去进行单元测试的实战.

一. 安装

我为本教程写一个示例库, 您可以直接跳过所有安装过程, 安装依赖后运行该示例项目:

如果想一步步进行安装, 也可以跟着下面的步骤进行操作:

(一) 使用脚手架初始化vue项目(使用webpack模板)

//命令行中输入(默认阅读该文章的读者已经安装vue-cli和node环境)vue init webpack vueunittest

注意, 当询问到这一步Pick a test runner(Use arrow keys)时, 请选择使用Karma and Mocha

选择Karma ad Mocha

接下来的操作进入项目npm install安装相关依赖后(该步骤可能更会出现PhantomJS这个浏览器安装失败的报错, 不用理会, 因为  之后我们不使用这个浏览器),npm run build即可.

(二) 安装Karma-chrome-launch

接下来安装karma-chrome-launcher, 在命令行中输入

npm install karma-chrome-launcher --save-dev

然后在项目中找到test/unit/karma.conf.js文件, 将PhantomJS浏览器修改为Chrome不要问我为什么不使用PhantomJS, 因为经常莫名的错误, 改成Chrome就不会!!!)

//karma.conf.jsvarwebpackConfig=require('../../build/webpack.test.conf')module.exports=function(config){config.set({//browsers: ['PhantomJS'],browsers:['Chrome'],...})}

(三) 安装Vue-test-utils

安装Vue.js 官方的单元测试实用工具库, 在命令行输入:

npm install--save-dev vue-test-utils

(四) 执行npm run unit

当你完成以上两步的时候, 你就可以在命令行执行npm run unit尝鲜你的第一次单元测试了, Vue脚手架已经初始化了一个HelloWorld.spec.js的测试文件去测试HelloWrold.vue, 你可以在test/unit/specs/HelloWorld.spec.js下找到这个测试文件.(提示: 将来所有的测试文件, 都将放specs这个目录下, 并以测试脚本名.spec.js结尾命名!)

在命令行输入npm run unit, 当你看到下图所示的一篇绿的时候, 说明你的单元测试通过了!

第一次单元测试测试通过

二. 测试工具的使用方法

下面是一个Counter.vue文件, 我将以该文件为基础讲解项目中测试工具的使用方法.

//Counter.vue<template><div><h3>Counter.vue</h3>{{count}}<button @click="increment">自增</button></div></template><script>exportdefault{data(){return{count:0}},methods:{increment(){this.count++}}}</script>

(一) Mocha框架

1. Mocha测试脚本的写法

Mocha的作用是运行测试脚本, 要对上面Counter.vue进行测试, 我们就要写测试脚本, 通常测试脚本应该与Vue组件名相同, 后缀为spec.js. 比如,Counter.vue组件的测试脚本名字就应该为Counter.spec.js

//Counter.spec.jsimportVuefrom'vue'importCounterfrom'@/components/Counter'describe('Counter.vue',()=>{it('点击按钮后, count的值应该为1',()=>{//获取组件实例constConstructor=Vue.extend(Counter);//挂载组件constvm=newConstructor().$mount();//获取buttonconstbutton=vm.$el.querySelector('button');//新建点击事件constclickEvent=newwindow.Event('click');//触发点击事件button.dispatchEvent(clickEvent);//监听点击事件vm._watcher.run();// 断言:count的值应该是数字1expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);})})

上面这段代码就是一个测试脚本.测试脚本应该包含一个或多个describe, 每个describe块应该包括一个或多个it块

describe块称为"测试套件"(test suite), 表示一组相关的测试. 它是一个函数, 第一个参数是测试套件的名称(通常写测试组件的名称, 这里即为Counter.js), 第二个参数是一个实际执行的函数.

it块称为"测试用例"(test case), 表示一个单独的测试, 是测试的最小单位. 它也是一个函数, 第一个参数是测试用例的名称(通常描述你的断言结果, 这里即为"点击按钮后, count的值应该为1"), 第二个参数是一个实际执行的函数.

2. Mocha进行异步测试

我们在Counter.vue组件中添加一个按钮, 并添加一个异步自增的方法为incrementByAsync, 该函数设置一个延时器, 1000ms后count自增1.

<template>...<button @click="increment">自增</button><button @click="incrementByAsync">异步自增</button>...<template><script>...methods:{...incrementByAsync(){window.setTimeout(()=>{this.count++;},1000)}}</script>

给测试脚本中新增一个测试用例, 也就是it()

it('count异步更新, count的值应该为1',(done)=>{///获取组件实例constConstructor=Vue.extend(Counter);//挂载组件constvm=newConstructor().$mount();//获取buttonconstbutton=vm.$el.querySelectorAll('button')[1];//新建点击事件constclickEvent=newwindow.Event('click');//触发点击事件button.dispatchEvent(clickEvent);//监听点击事件vm._watcher.run();//1s后进行断言window.setTimeout(()=>{// 断言:count的值应该是数字1expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);done();},1000);})

Mocha中的异步测试, 需要给it()内函数的参数中添加一个done, 并在异步执行完后必须调用done(), 如果不调用done(), 那么Mocha会在2000ms后报错且本次单元测试测试失败(mocha默认的异步测试超时上线为2000ms), 错误信息如下:

未调用done()的报错

3. Mocha的测试钩子

如果大家对于vue的mounted(),created()钩子能够理解的话, 对Mocha的钩子也很容易理解, Mocha在describe块中提供了四个钩子:before(),after(),beforeEach(),afterEach(). 它们会在以下时间执行

describe('钩子说明',function(){before(function(){// 在本区块的所有测试用例之前执行});after(function(){// 在本区块的所有测试用例之后执行});beforeEach(function(){// 在本区块的每个测试用例之前执行});afterEach(function(){// 在本区块的每个测试用例之后执行});});

上述就是Mocha的基本使用介绍, 如果想了解Mocha的更多使用方法, 可以查看下面的文档和一篇阮一峰的Mocha教程:

Mocha官方文档 :https://mochajs.org/

Mocha官方文档翻译 ://www.greatytc.com/p/9c78548caffa

阮一峰 - 测试框架 Mocha 实例教程 :http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html

(二) Chai断言库

上面的测试用例中, 以expect()方法开头的就是断言.

expect(Number(vm.$el.querySelector('.num').textContent)).to.equal(1);

所谓断言, 就是判断源码的实际执行结果与预期结果是否一致, 如果不一致, 就会抛出错误. 上面的断言的意思是指: 有.num这类名的节点的内容应该为数字1. 断言库库有很多种, Mocha并不限制你需要使用哪一种断言库,  Vue的脚手架提供的断言库是sino-chai, 是一个基于Chai的断言库, 并且我们指定使用的是它的expect断言风格.

expect断言风格的优点很接近于自然语言, 下面是一些例子

// 相等或不相等expect(1+1).to.be.equal(2);expect(1+1).to.be.not.equal(3);// 布尔值为trueexpect('hello').to.be.ok;expect(false).to.not.be.ok;// typeofexpect('test').to.be.a('string');expect({foo:'bar'}).to.be.an('object');expect(foo).to.be.an.instanceof(Foo);// includeexpect([1,2,3]).to.include(2);expect('foobar').to.contain('foo');expect({foo:'bar',hello:'universe'}).to.include.keys('foo');// emptyexpect([]).to.be.empty;expect('').to.be.empty;expect({}).to.be.empty;// matchexpect('foobar').to.match(/^foo/);

每一个it()所包裹的测试用例都应该有一句或多句断言,上面只是介绍了一部分的断言语法, 如果想要知道更多Chai的断言语法, 请查看以下的官方文档.

Chai官方文档:http://chaijs.com/

Chai官方文档翻译://www.greatytc.com/p/f200a75a15d2

(三) Vue-test-utils测试库

1. 在测试脚本中引入vue-test-utils

//Counter.spec.jsimportVuefrom'vue'importCounterfrom'@/components/Counter'//引入vue-test-utilsimport{mount}from'vue-test-utils'

2. 测试文本内容

下面我将在Counter.spec.js测试脚本中对Counter.vue中<h3>的文本内容进行测试, 大家可以直观的感受一下使用了Vue-test-utils后对.vue单文件组件的测试变得多么简单.

未使用vue-test-utils的测试用例:

it('未使用Vue-test-utils: 正确渲染h3的文字为Counter.vue',()=>{constConstructor=Vue.extend(Counter);constvm=newConstructor().$mount();constH3=vm.$el.querySelector('h3').textContent;expect(H3).to.equal('Counter.vue');})

使用了vue-test-utils的测试用例:

it('使用Vue-test-Utils: 正确渲染h3的文字为Counter.vue',()=>{constwrapper=mount(Counter);expect(wrapper.find('h3').text()).to.equal('Counter.vue');})

从上面的代码可以看出, vue-test-utils工具将该测试用例的代码量减少了一半, 如果是更复杂的测试用例, 那么代码量的减少将更为突出.  它可以让我们更专注于去写文件的测试逻辑, 将获取组件实例和挂载的繁琐的操作交由vue-test-utils去完成.

3. vue-test-utils的常用API

find(): 返回匹配选择器的第一个DOM节点或Vue组件的wrapper, 可以使用任何有效的选择器

text(): 返回wrapper的文本内容

html(): 返回wrapper DOM的HTML字符串

it('find()/text()/html()方法',()=>{constwrapper=mount(Counter);consth3=wrapper.find('h3');expect(h3.text()).to.equal('Counter.vue');expect(h3.html()).to.equal('<h3>Counter.vue</h3>');})

trigger(): 在该wrapper DOM节点上触发一个事件。

it('trigger()方法',()=>{constwrapper=mount(Counter);constbuttonOfSync=wrapper.find('.sync-button');buttonOfSync.trigger('click');buttonOfSync.trigger('click');constcount=Number(wrapper.find('.num').text());expect(count).to.equal(2);})

setData(): 设置data的属性并强制更新

it('setData()方法',()=>{constwrapper=mount(Counter);wrapper.setData({foo:'bar'});expect(wrapper.vm.foo).to.equal('bar');})

上面介绍了几个vue-test-utils提供的方法, 如果想深入学习vue-test-utils, 请阅读下面的官方文档:

vue-test-utils官方文档:https://vue-test-utils.vuejs.org/zh-cn/

三. 项目说明

该项目模仿了一个简单的微博, 在代码仓库下载后, 可直接通过npm run dev运行.

(一) 项目效果图

项目展示

(二) 项目中的交互逻辑和需求

在文本框中输入内容后点击"发布"按钮(1), 会新发布内容到微博列表中, 且个人头像等下的微博数量(6)会增加1个

当文本框中无内容时, 不能发布空微博到微博列表, 且弹出提示框, 叫用户输入内容

当点击"关注"(2), 个人头像下关注的数量(5)会增加1个, 且按钮内字体变成"取消关注"; 当点击"取消关注"(2), 个人头像下的数量(5)会减少1个, 且按钮内字体变成"关注"

当点击"收藏"(3)时, 我的收藏(7)会增加1个数量, 且按钮内文字变成"已收藏"; 点击"已收藏"(3)时, 我的收藏(7)会减少1个数量, 且按钮内文字变成"收藏"

当点击"赞"(4), 我的赞(8)会增加1个数量, 且按钮内文字变成"取消赞"; 点击"取消赞"(3)时, 我的赞(8)会减少1个数量, 且按钮内文字变成"赞"

(三) 项目源码

//SinaWeibo.vue<template><divclass="weibo-page"><nav><spanclass="weibo-logo"></span><divclass="search-wrapper"><input type="text"placeholder="大家正在搜: 李棠辉的文章好赞!"><img v-if="!iconActive"@mouseover="mouseOverToIcon"src="../../static/image/search.png"alt="搜索icon"><img v-if="iconActive"@mouseout="mouseOutToIcon"src="../../static/image/search-active.png"alt="搜索icon"></div></nav><divclass="main-container"><asideclass="aside-nav"><ul><li:class="{ active: isActives[indexOfContent] }"v-for="(content, indexOfContent) in asideTab":key="indexOfContent"@click="tabChange(indexOfContent)"><span>{{content}}</span><spanclass="count"><span v-if="indexOfContent === 1">({{collectNum}})</span><span v-if="indexOfContent === 2">({{likeNum}})</span></span></li></ul></aside><mainclass="weibo-content"><divclass="weibo-publish-wrapper"><img src="../../static/image/tell-people.png"></img><textarea v-model="newWeiboContent.content"></textarea><button @click="publishNewWeiboContent">发布</button></div><divclass="weibo-news"v-for="(news, indexOfNews) in weiboNews":key="indexOfNews"><divclass="content-wrapper"><divclass="news-title"><divclass="news-title__left"><img:src="news.imgUrl"><divclass="title-text"><divclass="title-name">{{news.name}}</div><divclass="title-time">{{news.resource}}</div></div></div><buttonclass="news-title__right add"v-if="news.attention === false"@click="attention(indexOfNews)"><iclass="fa fa-plus"></i>关注</button><buttonclass="news-title__right cancel"v-if="news.attention === true"@click="unAttention(indexOfNews)"><iclass="fa fa-close"></i>取消关注</button></div><divclass="news-content">{{news.content}}</div><divclass="news-image"v-if="news.images.length"><img            v-for="(img, indexOfImg) in news.images":key="indexOfImg":src="img"></div></div><ulclass="news-panel"><li @click="handleCollect(indexOfNews)"><iclass="fa fa-star-o":class="{collected: news.collect }"></i>{{news.collect?"已收藏":'收藏'}}</li><li><iclass="fa fa-external-link"></i>转发</li><li><iclass="fa fa-commenting-o"></i>评论</li><li @click="handleLike(indexOfNews)"><iclass="fa fa-thumbs-o-up":class="{liked: news.like}"></i>{{news.like?'取消赞':'赞'}}</li></ul></div></main><asideclass="aside-right"><divclass="profile-wrapper"><divclass="profile-top"><img src="../../static/image/profile.jpg"></div><divclass="profile-bottom"><divclass="profile-name">Lee_tanghui</div><ulclass="profile-info"><li                v-for="(profile, indexOfProfile) in profileData":key="indexOfProfile"><divclass="number">{{profile.num}}</div><divclass="text">{{profile.text}}</div></li></ul></div></div></aside></div><footer>Wish you like my blog!---LITANGHUI</footer></div></template><script>//引入假数据import*asmockDatafrom'../mock-data.js'exportdefault{mounted(){//模拟获取数据this.profileData=mockData.profileData;this.weiboNews=mockData.weiboNews;this.collectNum=mockData.collectNum;this.likeNum=mockData.likeNum;},data(){return{iconActive:false,asideTab:["首页","我的收藏","我的赞"],isActives:[true,false,false],profileData:[],weiboNews:[],collectNum:0,likeNum:0,newWeiboContent:{imgUrl:'../../static/image/profile.jpg',name:'Lee_tanghui',resource:'刚刚 来自 网页版微博',content:'',images:[]},}},methods:{mouseOverToIcon(){this.iconActive=true;},mouseOutToIcon(){this.iconActive=false;},tabChange(indexOfContent){this.isActives.forEach((item,index)=>{index===indexOfContent?this.$set(this.isActives,index,true):this.$set(this.isActives,index,false);})},publishNewWeiboContent(){if(!this.newWeiboContent.content){alert('请输入内容!')return;}constnewWeibo=JSON.parse(JSON.stringify(this.newWeiboContent));this.weiboNews.unshift(newWeibo);this.newWeiboContent.content='';this.profileData[2].num++;},attention(index){this.weiboNews[index].attention=true;this.profileData[0].num++;},unAttention(index){this.weiboNews[index].attention=false;this.profileData[0].num--;},handleCollect(index){this.weiboNews[index].collect=!this.weiboNews[index].collect;this.weiboNews[index].collect?this.collectNum++:this.collectNum--;},handleLike(index){this.weiboNews[index].like=!this.weiboNews[index].like;this.weiboNews[index].like?this.likeNum++:this.likeNum--;}}}</script><style lang="less">//css部分略</style>

四. 项目单元测试脚本实战

我们将以上文提到的"项目中的交互逻辑和需求"为基础, 为SinaWeibo.vue编写测试脚本, 下面我将展示测试用例编写过程:

1.在文本框中输入内容后点击"发布"按钮(1), 会新发布内容到微博列表中, 且个人头像等下的微博数量(6)会增加1个

it('点击发布按钮,发布新内容&个人微博数量增加1个',()=>{constwrapper=mount(SinaWeibo);consttextArea=wrapper.find('.weibo-publish-wrapper textarea');constbuttonOfPublish=wrapper.find('.weibo-publish-wrapper button');constlengthOfWeiboNews=wrapper.vm.weiboNews.length;constcountOfMyWeibo=wrapper.vm.profileData[2].num;//设置textArea的绑定数据wrapper.setData({newWeiboContent:{imgUrl:'../../static/image/profile.jpg',name:'Lee_tanghui',resource:'刚刚 来自 网页版微博',content:'欢迎来到我的微博',images:[]}});//触发点击事件buttonOfPublish.trigger('click');constlengthOfWeiboNewsAfterPublish=wrapper.vm.weiboNews.length;constcountOfMyWeiboAfterPublish=wrapper.vm.profileData[2].num;//断言: 发布新内容expect(lengthOfWeiboNewsAfterPublish).to.equal(lengthOfWeiboNews+1);//断言: 个人微博数量增加1个expect(countOfMyWeiboAfterPublish).to.equal(countOfMyWeibo+1);})

测试结果:

通过测试

2.当文本框中无内容时, 不能发布空微博到微博列表, 且弹出提示框, 叫用户输入内容

it('当文本框中无内容时, 不能发布空微博到微博列表, 且弹出提示框',()=>{constwrapper=mount(SinaWeibo);consttextArea=wrapper.find('.weibo-publish-wrapper textarea');constbuttonOfPublish=wrapper.find('.weibo-publish-wrapper button');constlengthOfWeiboNews=wrapper.vm.weiboNews.length;constcountOfMyWeibo=wrapper.vm.profileData[2].num;//设置textArea的绑定数据为空wrapper.setData({newWeiboContent:{imgUrl:'../../static/image/profile.jpg',name:'Lee_tanghui',resource:'刚刚 来自 网页版微博',content:'',images:[]}});//触发点击事件buttonOfPublish.trigger('click');constlengthOfWeiboNewsAfterPublish=wrapper.vm.weiboNews.length;constcountOfMyWeiboAfterPublish=wrapper.vm.profileData[2].num;//断言: 没有发布新内容expect(lengthOfWeiboNewsAfterPublish).to.equal(lengthOfWeiboNews);//断言: 个人微博数量不变expect(countOfMyWeiboAfterPublish).to.equal(countOfMyWeibo);})

测试结果:

image_1c2l52h0i11e51fjqm9o8751m7hm.png-32.4kB

3.当点击"关注"(2), 个人头像下关注的数量(5)会增加1个, 且按钮内字体变成"取消关注"; 当点击"取消关注"(2), 个人头像下的数量(5)会减少1个, 且按钮内字体变成"关注"

it('当点击"关注", 个人头像下关注的数量会增加1个, 且按钮内字体变成"取消关注"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfAddAttendion=wrapper.find('.add');constcountOfMyAttention=wrapper.vm.profileData[0].num;//触发事件buttonOfAddAttendion.trigger('click');constcountOfMyAttentionAfterClick=wrapper.vm.profileData[0].num;//断言: 个人头像下关注的数量会增加1个expect(countOfMyAttentionAfterClick).to.equal(countOfMyAttention+1);//断言: 按钮内字体变成"取消关注expect(buttonOfAddAttendion.text()).to.equal('取消关注');})

it('当点击"取消关注", 个人头像下关注的数量会减少1个, 且按钮内字体变成"关注"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfUnAttendion=wrapper.find('.cancel');constcountOfMyAttention=wrapper.vm.profileData[0].num;//触发事件buttonOfUnAttendion.trigger('click');constcountOfMyAttentionAfterClick=wrapper.vm.profileData[0].num;//断言: 个人头像下关注的数量会增加1个expect(countOfMyAttentionAfterClick).to.equal(countOfMyAttention-1);//断言: 按钮内字体变成"取消关注expect(buttonOfUnAttendion.text()).to.equal('关注');})

测试结果:

image_1c2lbcvod1d7ton71mod1i6b1bt13.png-62.3kB

4.当点击"收藏"(3)时, 我的收藏(7)会增加1个数量, 且按钮内文字变成"已收藏"; 点击"已收藏"(3)时, 我的收藏(7)会减少1个数量, 且按钮内文字变成"收藏"

it('当点击"收藏"时, 我的收藏会增加1个数量, 且按钮内文字变成"已收藏"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfCollect=wrapper.find('.collectWeibo');constcountOfMyCollect=Number(wrapper.find('.collect-num span').text());//触发点击事件buttonOfCollect.trigger('click');constcountOfMyCollectAfterClick=Number(wrapper.find('.collect-num span').text());//断言: 我的收藏数量会加1expect(countOfMyCollectAfterClick).to.equal(countOfMyCollect+1);//断言: 按钮内文字变成已收藏expect(buttonOfCollect.text()).to.equal('已收藏');})

it('当点击"已收藏"时, 我的收藏会减少1个数量, 且按钮内文字变成"收藏"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfUnCollect=wrapper.find('.uncollectWeibo');constcountOfMyCollect=Number(wrapper.find('.collect-num span').text());//触发点击事件buttonOfUnCollect.trigger('click');constcountOfMyCollectAfterClick=Number(wrapper.find('.collect-num span').text());//断言: 我的收藏数量会减1expect(countOfMyCollectAfterClick).to.equal(countOfMyCollect-1);//断言: 按钮内文字变成已收藏expect(buttonOfUnCollect.text()).to.equal('收藏');})

测试结果:

image_1c2lbdfe9i5jrja10cc447rgj1g.png-88.6kB

5.当点击"赞"(4), 我的赞(8)会增加1个数量, 且按钮内文字变成"取消赞"; 点击"取消赞"(3)时, 我的赞(8)会减少1个数量, 且按钮内文字变成"赞"

it('当点击"赞", 我的赞会增加1个数量, 且按钮内文字变成"取消赞"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfLike=wrapper.find('.dislikedWeibo');constcountOfMyLike=Number(wrapper.find('.like-num span').text());//触发点击事件buttonOfLike.trigger('click');constcountOfMyLikeAfterClick=Number(wrapper.find('.like-num span').text());//断言: 我的赞会增加1个数量expect(countOfMyLikeAfterClick).to.equal(countOfMyLike+1);//断言: 按钮内文字变成取消赞expect(buttonOfLike.text()).to.equal('取消赞');});

it('当点击"取消赞", 我的赞会减少1个数量, 且按钮内文字变成"赞"',()=>{constwrapper=mount(SinaWeibo);constbuttonOfDislike=wrapper.find('.likedWeibo');constcountOfMyLike=Number(wrapper.find('.like-num span').text());//触发点击事件buttonOfDislike.trigger('click');constcountOfMyLikeAfterClick=Number(wrapper.find('.like-num span').text());//断言: 我的赞会增加1个数量expect(countOfMyLikeAfterClick).to.equal(countOfMyLike-1);//断言: 按钮内文字变成取消赞expect(buttonOfDislike.text()).to.equal('赞');});

测试结果

作者:李棠辉

链接://www.greatytc.com/p/38a37d5fccb2

来源:简书

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

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