SharePreference單元測(cè)試超級(jí)簡(jiǎn)單!

吐槽Robolectric

如果你讀過(guò)筆者的《Android單元測(cè)試 - Sqlite奠宜、SharedPreference包颁、Assets瞻想、文件操作 怎么測(cè)?》娩嚼,就知道我們可以用Robolectric去做SharePreference單元測(cè)試蘑险。但筆者越來(lái)越覺得Robolectric非常麻煩,主要以下幾點(diǎn):

1.對(duì)初學(xué)者門檻高
2.運(yùn)行效率低下

第一次使用Robolectric的同學(xué)岳悟,必然會(huì)卡在下載依賴的步驟佃迄,這一步讓多少同學(xué)放棄或者延遲學(xué)習(xí)robolectric。讀者可以參考《加速Robolectric下載依賴庫(kù)及原理剖析》贵少,徹底解決這個(gè)問(wèn)題呵俏。

其次,就是配置麻煩滔灶,從2.x到3.x版本普碎,配置一直改動(dòng)(其實(shí)是越來(lái)越精簡(jiǎn)),2.x版本的配置到3.x版本录平,就有問(wèn)題麻车,不得不重新看官方文檔如何配置。有時(shí)不知道是改了gradle版本還是什么原因斗这,配置沒(méi)變动猬,就給你報(bào)錯(cuò),常見的"No such manifest file: build\intermediates\bundles\debug\AndroidManifest.xml"......

至于運(yùn)行效率涝影,由于Robolectric是一個(gè)大而全的框架枣察,單元測(cè)試到UI測(cè)試都能做,運(yùn)行時(shí)先解析燃逻、加載一大堆東西序目,才給你跑測(cè)試。筆者研究過(guò)源碼伯襟,前期解析慢主要是UI方面猿涨,如果只是測(cè)SharePreferenceSQLiteDatabase根本不需要,就想不明白R(shí)obolectric團(tuán)隊(duì)為什么不把SharePreferenceSQLiteDatabase配置分離出來(lái)姆怪,好讓單元測(cè)試跑快一點(diǎn)叛赚。

簡(jiǎn)單實(shí)驗(yàn),跑一個(gè)什么都不做的robolectric test case:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class)
public class RoboCase {
    @Test
    public void testRobo() {

    }
}
Robo Test Case

盡管你什么都不做稽揭,不好意思俺附,Robolectric就得運(yùn)行3秒!而且隨著工程代碼增加溪掀,這個(gè)時(shí)間有增無(wú)減事镣。如果跑一個(gè)什么都不做的Junit單元測(cè)試,1ms不到揪胃。筆者本文介紹的方法璃哟,跑簡(jiǎn)單的運(yùn)行測(cè)試時(shí)間在10~1000ms不等氛琢,視乎測(cè)試代碼復(fù)雜度,最快比Robolectric快140+倍随闪。


理解SharedPreferences

我們通過(guò)Context獲取SharedPreferences

Context context;
SharedPreferences sharePref = context.getSharedPreferences("name", Context.MODE_PRIVATE);

getSharedPreferencesnamemode參數(shù)阳似,傳不同的name獲取不同的SharedPreferences

SharedPreferences源碼:

public interface SharedPreferences {

    public interface OnSharedPreferenceChangeListener {
        void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key);
    }

    public interface Editor {
        Editor putString(String key, @Nullable String value);

        Editor putStringSet(String key, @Nullable Set<String> values);

        Editor putInt(String key, int value);

        Editor putLong(String key, long value);

        Editor putFloat(String key, float value);

        Editor putBoolean(String key, boolean value);

        Editor remove(String key);

        Editor clear();

        boolean commit();

        void apply();
    }

    Map<String, ?> getAll();

    @Nullable
    String getString(String key, @Nullable String defValue);

    @Nullable
    Set<String> getStringSet(String key, @Nullable Set<String> defValues);

    int getInt(String key, int defValue);

    long getLong(String key, long defValue);

    float getFloat(String key, float defValue);

    boolean getBoolean(String key, boolean defValue);

    boolean contains(String key);

    Editor edit();

    void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);

    void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener);
}

SharedPreferences實(shí)際上只是一個(gè)接口铐伴,我們獲取的對(duì)象撮奏,是繼承該接口的android.app.SharedPreferencesImpl。Android sdk沒(méi)有提供這個(gè)類当宴,讀者可閱讀源碼:SharedPreferencesImpl.java挽荡。

從功能上看,SharedPreferences就是簡(jiǎn)單的kev-value數(shù)據(jù)庫(kù)即供,在app運(yùn)行時(shí),對(duì)SharedPreferences儲(chǔ)存于微、讀取數(shù)據(jù)逗嫡,會(huì)存放在Android手機(jī)該app空間的文件里。

單元測(cè)試思路

首先株依,單元測(cè)試原則是每個(gè)測(cè)試用例的數(shù)據(jù)獨(dú)立驱证。因此,前一個(gè)測(cè)試用例在SharedPreferences儲(chǔ)存的數(shù)據(jù)恋腕,下一個(gè)用例不應(yīng)該讀取到抹锄,SharedPreferences就沒(méi)有必要真的把數(shù)據(jù)儲(chǔ)存在文件了,只需要存放在jvm內(nèi)存就足夠荠藤。

既然SharedPreferences的功能用內(nèi)存實(shí)現(xiàn)伙单,那么java代碼就能輕易實(shí)現(xiàn)key-value儲(chǔ)存,原理跟java.util.Map如出一轍哈肖。

代碼實(shí)現(xiàn)SharedPreferences

ShadowSharedPreferences:

public class ShadowSharedPreference implements SharedPreferences {

    Editor editor;

    List<OnSharedPreferenceChangeListener> mOnChangeListeners = new ArrayList<>();
    Map<String, Object>                    map                = new ConcurrentHashMap<>();

    public ShadowSharedPreference() {
        editor = new ShadowEditor(new EditorCall() {

            @Override
            public void apply(Map<String, Object> commitMap, List<String> removeList, boolean commitClear) {
                Map<String, Object> realMap = map;

                // clear
                if (commitClear) {
                    realMap.clear();
                }

                // 移除元素
                for (String key : removeList) {
                    realMap.remove(key);

                    for (OnSharedPreferenceChangeListener listener : mOnChangeListeners) {
                        listener.onSharedPreferenceChanged(ShadowSharedPreference.this, key);
                    }
                }

                // 添加元素
                Set<String> keys = commitMap.keySet();

                // 對(duì)比前后變化
                for (String key : keys) {
                    Object lastValue = realMap.get(key);
                    Object value     = commitMap.get(key);

                    if ((lastValue == null && value != null) || (lastValue != null && value == null) || !lastValue.equals(value)) {
                        for (OnSharedPreferenceChangeListener listener : mOnChangeListeners) {
                            listener.onSharedPreferenceChanged(ShadowSharedPreference.this, key);
                        }
                    }
                }

                realMap.putAll(commitMap);
            }
        });
    }

    public Map<String, ?> getAll() {
        return new HashMap<>(map);
    }

    public String getString(String key, @Nullable String defValue) {
        if (map.containsKey(key)) {
            return (String) map.get(key);
        }

        return defValue;
    }

    public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        if (map.containsKey(key)) {
            return new HashSet<>((Set<String>) map.get(key));
        }

        return defValues;
    }

    public int getInt(String key, int defValue) {
        if (map.containsKey(key)) {
            return (Integer) map.get(key);
        }

        return defValue;
    }

    public long getLong(String key, long defValue) {
        if (map.containsKey(key)) {
            return (Long) map.get(key);
        }

        return defValue;
    }

    public float getFloat(String key, float defValue) {
        if (map.containsKey(key)) {
            return (Float) map.get(key);
        }

        return defValue;
    }

    public boolean getBoolean(String key, boolean defValue) {
        if (map.containsKey(key)) {
            return (Boolean) map.get(key);
        }

        return defValue;
    }

    public boolean contains(String key) {
        return map.containsKey(key);
    }

    public Editor edit() {
        return editor;
    }

    /**
     * 監(jiān)聽對(duì)應(yīng)的key值的變化吻育,只有當(dāng)key對(duì)應(yīng)的value值發(fā)生變化時(shí),才會(huì)觸發(fā)
     *
     * @param listener
     */
    @Override
    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mOnChangeListeners.add(listener);
    }

    @Override
    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
        mOnChangeListeners.remove(listener);
    }

    interface EditorCall {
        void apply(Map<String, Object> map, List<String> removeList, boolean commitClear);
    }

    public class ShadowEditor implements SharedPreferences.Editor {

        boolean commitClear;

        Map<String, Object> map        = new ConcurrentHashMap<>();
        /**
         * 待移除列表
         */
        List<String>        removeList = new ArrayList<>();

        EditorCall mCall;

        public ShadowEditor(EditorCall call) {
            this.mCall = call;
        }

        public ShadowEditor putString(String key, @Nullable String value) {
            map.put(key, value);
            return this;
        }

        public ShadowEditor putStringSet(String key, @Nullable Set<String> values) {
            map.put(key, new HashSet<>(values));
            return this;
        }

        public ShadowEditor putInt(String key, int value) {
            map.put(key, value);
            return this;
        }

        public ShadowEditor putLong(String key, long value) {
            map.put(key, value);
            return this;
        }

        public ShadowEditor putFloat(String key, float value) {
            map.put(key, value);
            return this;
        }

        public ShadowEditor putBoolean(String key, boolean value) {
            map.put(key, value);
            return this;
        }

        public ShadowEditor remove(String key) {
            map.remove(key);
            removeList.add(key);
            return this;
        }

        public ShadowEditor clear() {
            commitClear = true;
            map.clear();
            removeList.clear();
            return this;
        }

        public boolean commit() {
            try {
                apply();
                return true;
            } catch (Exception e) {
                e.printStackTrace();
                return false;
            }
        }

        public void apply() {
            mCall.apply(map, removeList, commitClear);

            // 每次提交清空緩存數(shù)據(jù)
            map.clear();
            commitClear = false;
            removeList.clear();
        }
    }
}

SharePreferenceHelper:

public class SharePreferenceHelper {

    public static SharedPreferences newInstance() {
        return new ShadowSharePreference();
    }
}

只需要兩個(gè)類淤井,準(zhǔn)備工作就大功告成了布疼,非常簡(jiǎn)單!

跑單元測(cè)試

BookDAO:

public class BookDAO {

    SharedPreferences        mSharedPre;
    SharedPreferences.Editor mEditor;

    // 單元測(cè)試調(diào)用币狠,注意聲明protected
    protected BookDAO(SharedPreferences sharedPre) {
        this.mSharedPre = sharedPre;
        this.mEditor    = sharedPre.edit();
    }

    // 正常代碼調(diào)用
    public BookDAO(Context context) {
        this(context.getSharedPreferences("book", Context.MODE_PRIVATE));
    }

    /**
     * 設(shè)置某book是否已讀
     *
     * @param bookId 書本id
     * @param isRead 是否已讀
     */
    public void setBookRead(int bookId, boolean isRead) {
        mEditor.putBoolean(String.valueOf(bookId), isRead);
        mEditor.commit();
    }

    /**
     * book是否已讀
     *
     * @param bookId 書本id
     * @return
     */
    public boolean isBookRead(int bookId) {
        return mSharedPre.getBoolean(String.valueOf(bookId), false);
    }
}

BookDAO有兩個(gè)構(gòu)造方法游两,BookDAO(SharedPreferences sharedPre)BookDAO(Context context),由于單元測(cè)試沒(méi)有Context漩绵,因此直接創(chuàng)建SharedPreferences對(duì)象即可贱案。

BookDAOTest單元測(cè)試:

public class BookDAOTest {

    BookDAO bookDAO;

    @Before
    public void setUp() throws Exception {
        bookDAO = new BookDAO(SharePreferenceHelper.newInstance());
    }

    @Test
    public void isBookRead() throws Exception {
        int bookId = 10;

        // 未讀
        Assert.assertFalse(bookDAO.isBookRead(bookId));

        // 設(shè)置已讀
        bookDAO.setBookRead(bookId, true);

        // 已讀
        Assert.assertTrue(bookDAO.isBookRead(bookId));
    }
}
BookDAO Test Case

僅需要12ms,非辰バ校快轰坊,而且不需要任何配置铸董。

進(jìn)階

場(chǎng)景測(cè)試

你本來(lái)有BookDAO,后來(lái)重構(gòu)肴沫,需要新增或者拋棄一些方法或者其他原因粟害,寫一個(gè)BookDAOV2。這個(gè)BookDAOV2BookDAO的數(shù)據(jù)共享颤芬,意味著用同一個(gè)SharedPreferences悲幅。

單元測(cè)試怎么寫呢?

public class BookDAOV2 {

    SharedPreferences        mSharedPre;
    SharedPreferences.Editor mEditor;

    protected BookDAOV2(SharedPreferences sharedPre) {
        this.mSharedPre = sharedPre;
        this.mEditor = sharedPre.edit();
    }
    
    public BookDAOV2(Context context) {
        // 與BookDAO使用同一個(gè)SharedPreferences
        this(context.getSharedPreferences("book", Context.MODE_PRIVATE));
    }

    public void clearAllRead() {
        mEditor.clear();
        mEditor.commit();
    }
}

測(cè)試用例:

public class BookUpdateTest {

    BookDAO   bookDAO;
    BookDAOV2 bookDAOV2;

    @Before
    public void setUp() throws Exception {
        SharedPreferences sharedPref = SharedPreferencesHelper.newInstance();

        bookDAO = new BookDAO(sharedPref);
        bookDAOV2 = new BookDAOV2(sharedPref);
    }

    @Test
    public void testClearAllRead() {
        int bookId = 10;

        // 設(shè)置已讀
        bookDAO.setBookRead(bookId, true);

        // 已讀
        Assert.assertTrue(bookDAO.isBookRead(bookId));

        // DAOV2 清除已讀
        bookDAOV2.clearAllRead();

        // 未讀
        Assert.assertFalse(bookDAO.isBookRead(bookId));
    }
}

但是這樣不太優(yōu)雅站蝠,能不能調(diào)用SharedPreferencesHelper同一個(gè)方法汰具,返回同一個(gè)SharedPreferences呢?

通過(guò)name獲取不同SharedPreferences

context.getSharedPreferences(name, mode)可以改變name類獲取不同SharedPreferences對(duì)象菱魔,這些SharedPreferences彼此數(shù)據(jù)獨(dú)立留荔。

因此,我們?cè)?code>SharePreferenceHelper加兩個(gè)靜態(tài)方法:

public class SharePreferenceHelper {

    private static Map<String, SharedPreferences> map = new ConcurrentHashMap<>();

    public static SharedPreferences getInstance(String name) {
        if (map.containsKey(name)) {
            return map.get(name);
        } else {
            SharedPreferences sharedPreferences = new ShadowSharePreference();

            map.put(name, sharedPreferences);

            return sharedPreferences;
        }
    }

    public static void clean() {
        map.clear();
    }
    ......
}

我們調(diào)用SharePreferenceHelper.getInstance(name)就可以獲取name對(duì)應(yīng)不同ShadowSharedPreferences澜倦。

跑個(gè)測(cè)試:

public class MultipleSharedPrefTest {

    @Test
    public void testSampleSharedPrefer() {
        SharedPreferences sharedPref0 = SharedPreferenceHelper.getInstance("name");
        SharedPreferences sharedPref1 = SharedPreferenceHelper.getInstance("name");

        Assert.assertEquals(sharedPref0, sharedPref1);
    }

    @Test
    public void testDifferentSharedPref() {
        SharedPreferences sharedPref0 = SharedPreferenceHelper.getInstance("name");
        SharedPreferences sharedPref1 = SharedPreferenceHelper.getInstance("other");

        // 不同SharedPreferences
        Assert.assertNotEquals(sharedPref0, sharedPref1);
    }
}

結(jié)果當(dāng)然是兩個(gè)都pass啦聚蝶!

處理Test Case前后數(shù)據(jù)干擾

運(yùn)行一次單元測(cè)試,無(wú)論Test Case多少藻治,jvm只啟動(dòng)一次碘勉,因此,靜態(tài)變量就會(huì)一直存在桩卵,直到該次單元測(cè)試完成验靡。問(wèn)題就出現(xiàn)了:上面介紹的SharedPreferenceHelper.getInstance(name),是通過(guò)static Map<String, SharedPreferences>緩存SharedPreferences對(duì)象雏节,所以胜嗓,同一次單元測(cè)試,上一個(gè)Test Case儲(chǔ)存的數(shù)據(jù)矾屯,會(huì)影響下一個(gè)Test Case兼蕊。

下面的單元測(cè)試,先執(zhí)行testA()件蚕,儲(chǔ)存key=1孙技,在執(zhí)行testB():

@FixMethodOrder(MethodSorters.NAME_ASCENDING)  // 按case名稱字母順序排序
public class DistractionTest {

    SharedPreferences        mSharedPref;
    SharedPreferences.Editor mEditor;

    @Before
    public void setUp() throws Exception {
        mSharedPref = SharedPreferencesHelper.getInstance("name");
        mEditor = mSharedPref.edit();
    }

    @Test
    public void testA() {
        mEditor.putInt("key", 1);
        mEditor.commit();
    }

    @Test
    public void testB() {
        // testA()的數(shù)據(jù),不應(yīng)該影響testB()
        Assert.assertEquals(0, mSharedPref.getInt("key", 0));
    }
}

很遺憾排作,testA()的數(shù)據(jù)影響到testB():

java.lang.AssertionError:
Expected :0
Actual :1

at org.junit.Assert.assertEquals(Assert.java:631)
at com.sharepreference.library.DistractionTest.testB(DistractionTest.java:34)

數(shù)據(jù)干擾

因此牵啦,需要在Test Case tearDown()方法回調(diào)時(shí),調(diào)用SharedPreferenceHelper.clean()妄痪,再運(yùn)行一次:

@FixMethodOrder(MethodSorters.NAME_ASCENDING)  // 按case名稱字母順序排序
public class DistractionTest {
    ...

    @After
    public void tearDown() throws Exception {
        SharedPreferencesHelper.clean();
    }
    ...
}
排除干擾

統(tǒng)一處理tearDown()

如果我們每個(gè)Test Case都要寫testDown()處理SharedPreferences緩存哈雏,未免太不優(yōu)雅。我們可以借助TestRule類完成。

SharedPrefRule:

public class SharedPrefRule extends ExternalResource {

    @Override
    protected void after() {
        // 每測(cè)試完一個(gè)用例方法裳瘪,就回調(diào)
        SharedPreferencesHelper.clean();
    }
}

SharedPrefCase:

public class SharedPrefCase {

    @Rule
    public SharedPrefRule rule = new SharedPrefRule();
    
    public SharedPreferences getInstance(String name) {
        return SharedPreferencesHelper.getInstance(name);
    }
}

于是土浸,我們所以SharedPrefences測(cè)試用例,都繼承SharedPrefCase:

public class MySharedPrefTest extends SharedPrefCase {

    SharedPreferences mSharedPre;

    @Before
    public void setUp() throws Exception {
        mSharedPre = getInstance("name");
    }
}

這樣彭羹,數(shù)據(jù)干擾的問(wèn)題就解決了黄伊。

修改BookUpdateTest

上文提到的BookDAOBookDAOV2單元測(cè)試,可以修改如下:

public class BookUpdateTest extends SharedPrefCase {

    @Before
    public void setUp() throws Exception {
        bookDAO = new BookDAO(getInstance("book"));
        bookDAOV2 = new BookDAOV2(getInstance("book"));
    }
}

比之前優(yōu)雅多了派殷。

Context獲取SharedPreferences

很多同學(xué)都會(huì)在Application.onCreate()時(shí)还最,在某個(gè)地方把ApplicationContext存起來(lái),方便其他地方獲取毡惜。然后拓轻,在DAO里面直接用這個(gè)Context獲取SharedPreferences。按照筆者的方法经伙,單元測(cè)試時(shí)扶叉,每個(gè)DAO都要傳一個(gè)新創(chuàng)建的SharedPreferences。但有的同學(xué)就是懶帕膜,有其他更好的方式嗎辜梳?

你的代碼可能是這樣:

public class ContextProvider {

    private static Context context;

    public static Context getContext() {
        return context;
    }

    public static void setContext(Context context) {
        ContextProvider.context = context;
    }
}
public class BookDAO {

    SharedPreferences mSharedPre;

    public BookDAO() {
        Context context = ContextProvider.getContext();
        mSharedPre      = context.getSharedPreferences("book", Context.MODE_PRIVATE);
    }
}

我們的問(wèn)題是,如何讓context.getSharedPreferences返回一個(gè)SharedPreferences泳叠。借助一下mockito來(lái)實(shí)現(xiàn),修改SharedPrefRule

public class SharedPrefRule extends ExternalResource {

    @Override
    protected void before() throws Throwable {
        Context context = mock(Context.class);

        // 調(diào)用context.getSharedPreferences(name)時(shí)茶宵,執(zhí)行SharedPreferencesHelper.getInstance(name)危纫,返回結(jié)果
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                String name = (String) invocation.getArguments()[0];
                return SharedPreferencesHelper.getInstance(name);
            }
        }).when(context).getSharedPreferences(anyString(), anyInt());
        
        // 設(shè)置Context
        ContextProvider.setContext(context);
    }
    ...
}

開源一個(gè)SharePreferences單元測(cè)試框架

本文的重頭戲——開源框架!

聽起來(lái)好像很屌的樣子乌庶,其實(shí)就是那么幾個(gè)類种蝶,見笑_. 上述的代碼,筆者整理成項(xiàng)目瞒大,在github開源螃征,并且發(fā)布到j(luò)itpack. 讀者可以免費(fèi)使用,通過(guò)gradle依賴透敌。

開源框架命名很頭痛盯滚,就叫SPTestFramework吧!

不需要Robolectric即可測(cè)試SharedPreferences酗电,SPTestFramework你值得擁有魄藕!

SPTestFramework項(xiàng)目是什么?

SPTestFramework(簡(jiǎn)稱SPTest)是一個(gè)SharedPreferences單元測(cè)試框架撵术。項(xiàng)目自帶單元測(cè)試背率,確保測(cè)試框架代碼質(zhì)量和功能正確。

同時(shí),歡迎各位同學(xué)使用寝姿、測(cè)試交排、提出問(wèn)題!

項(xiàng)目地址:https://github.com/kkmike999/SPTestFramework


關(guān)于作者

我是鍵盤男饵筑。
在廣州生活埃篓,悅跑圈Android工程師,猥瑣文藝碼農(nóng)翻翩。每天謀劃砍死產(chǎn)品經(jīng)理都许。喜歡科學(xué)、歷史嫂冻,玩玩投資胶征,偶爾旅行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末桨仿,一起剝皮案震驚了整個(gè)濱河市睛低,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌服傍,老刑警劉巖钱雷,帶你破解...
    沈念sama閱讀 217,907評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異吹零,居然都是意外死亡罩抗,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門灿椅,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)套蒂,“玉大人,你說(shuō)我怎么就攤上這事茫蛹〔俚叮” “怎么了?”我有些...
    開封第一講書人閱讀 164,298評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵婴洼,是天一觀的道長(zhǎng)骨坑。 經(jīng)常有香客問(wèn)我,道長(zhǎng)柬采,這世上最難降的妖魔是什么欢唾? 我笑而不...
    開封第一講書人閱讀 58,586評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮粉捻,結(jié)果婚禮上匈辱,老公的妹妹穿的比我還像新娘。我一直安慰自己杀迹,他們只是感情好亡脸,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,633評(píng)論 6 392
  • 文/花漫 我一把揭開白布押搪。 她就那樣靜靜地躺著,像睡著了一般浅碾。 火紅的嫁衣襯著肌膚如雪大州。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評(píng)論 1 302
  • 那天垂谢,我揣著相機(jī)與錄音厦画,去河邊找鬼。 笑死滥朱,一個(gè)胖子當(dāng)著我的面吹牛根暑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播徙邻,決...
    沈念sama閱讀 40,275評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼苇经,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼捎迫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,176評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瞪醋,失蹤者是張志新(化名)和其女友劉穎迅腔,沒(méi)想到半個(gè)月后牍帚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體银伟,經(jīng)...
    沈念sama閱讀 45,619評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,819評(píng)論 3 336
  • 正文 我和宋清朗相戀三年并徘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了遣钳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,932評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡麦乞,死狀恐怖耍贾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情路幸,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評(píng)論 5 346
  • 正文 年R本政府宣布付翁,位于F島的核電站简肴,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏百侧。R本人自食惡果不足惜砰识,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,265評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望佣渴。 院中可真熱鬧辫狼,春花似錦、人聲如沸辛润。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至真椿,卻和暖如春鹃答,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背突硝。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工测摔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人解恰。 一個(gè)月前我還...
    沈念sama閱讀 48,095評(píng)論 3 370
  • 正文 我出身青樓锋八,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親护盈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挟纱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,884評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容