Espresso UI自動(dòng)化測(cè)試框架

前言

Espresso是谷歌大力推薦的一套測(cè)試框架良价,從Android studio2.2版本開始驶臊,google就開始支持在as上espresso自動(dòng)生成單元測(cè)試代碼。

Espresso測(cè)試運(yùn)行速度很快局齿,它允許你在應(yīng)用程序處于靜止時(shí)操作和斷言等澈段。

Espresso面向那些認(rèn)為自動(dòng)化測(cè)試是開發(fā)聲明周期的一部分的開發(fā)人員悠菜,并且雖然它可以被用作黑盒測(cè)試,但是熟悉代碼庫(kù)的人可以解鎖Espresso的全部功能败富。

簡(jiǎn)單實(shí)例

Espresso非常簡(jiǎn)單醫(yī)用悔醋,并且可定制,能清楚的表明期望兽叮,交互和斷言芬骄。

@Test
public void test(){
    onView(withId(R.id.test)).perform(click()).check(matches(withText("clicked")));
}

每次調(diào)用onView()時(shí),Espresso都會(huì)等待執(zhí)行相應(yīng)的操作和斷言鹦聪,知道滿足一下條件為止账阻。

  • The message queue is empty.
  • There are no instances of AsyncTask currently executing a task.
  • All developer-defined idling resources are idle.

依賴的類庫(kù)

  • espresso-core 包含基礎(chǔ)的視圖匹配器,操作和斷言庫(kù)
  • espresso-web 包含webview的測(cè)試庫(kù)
  • espresso-idling-resource 包含和后臺(tái)作業(yè)的同步的機(jī)制
  • espresso-contrib 包括DatePicker, RecyclerView and Drawer actions, accessibility checks, and CountingIdlingResource這些的擴(kuò)展庫(kù)
  • espresso-intent 包括與意圖有關(guān)的測(cè)試庫(kù)
  • espresso-remote Espresso多處理功能的位置

設(shè)置和使用

1. 設(shè)置測(cè)試環(huán)境

為了避免測(cè)試過(guò)程出現(xiàn)問(wèn)題泽本,最好關(guān)閉開發(fā)者選項(xiàng)中的下邊三個(gè)設(shè)置淘太。

  • Window animation scale
  • Transition animation scale
  • Animator duration scale
2. 配置gradle
dependencies{
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test:rules:1.0.2'
}
...
android{
    defaultConfig{
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
}

注意: Android Studio現(xiàn)在新建項(xiàng)目一般都會(huì)依賴部分Espresso的庫(kù),但是有的仍需要引入部分庫(kù)观挎,比如我的Android studio 3.0.1需要引入'com.android.support.test:rules:1.0.2'庫(kù)

3. 編寫測(cè)試代碼
@LarggeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest{
    @Rule
    public ActivityTestRule mActivityRule = new ActivityTestRule(MainActivity.class);
    
    @Test
    public void runTest(){
        onView(withId(R.id.test)).perform(click()).check(matches(withText("2")));
    }
}

注意: ActivityTestRule需要導(dǎo)入上一步的一個(gè)rules庫(kù)琴儿。
如果onView等方法沒(méi)有代碼提示,可以手動(dòng)先導(dǎo)入相關(guān)類庫(kù)嘁捷。

4. 運(yùn)行測(cè)試用例

可以使用as的運(yùn)行功能也可以使用命令行方式

在Android Studio中:
  1. 打開Run -> Edit Configurations
  2. 添加一個(gè)新的測(cè)試配置
  3. 選擇module
  4. 添加一個(gè)指定的runner:android.support.test.runner.AndroidJUnitRunner
  5. 運(yùn)行新配置
在命令行中

執(zhí)行以下代碼:

./gradlew connectedAndroidTest

Espresso API

Espresso API鼓勵(lì)開發(fā)者根據(jù)用戶與應(yīng)用程序交互時(shí)可能執(zhí)行的操作進(jìn)行思考-定位UI元素并與之進(jìn)行交互造成,同時(shí)框架阻止直接訪問(wèn)應(yīng)用程序的活動(dòng)和視圖。

API中主要包括以下的包:

  • Espresso 與視圖交互的入口(onView(),onData()),同時(shí)公開不一定與視圖綁定的api雄嚣,例如pressBack()
  • ViewMatchers 一組實(shí)現(xiàn)了Matcher< ? super View>接口的對(duì)象晒屎,你可以將這個(gè)作為篩選條件在onView中定位視圖
  • ViewAction 一組可以被傳遞給ViewInteraction.perform()的ViewAction對(duì)象,比如click()
  • ViewAssertions 一組可以被傳遞給ViewInteraction.check()方法的ViewAssertion對(duì)象缓升,大多數(shù)情況下鼓鲁,你需要使用這個(gè)匹配斷言,當(dāng)一個(gè)viewmatcher去斷言一個(gè)view的狀態(tài)的時(shí)候港谊。

實(shí)例:

onView(withId(R.id.test)) // withId() is a ViewMatcher
    .perform(click()) // click() is a ViewAction
    .check(matches(isDisplayed())); // matches(isDisplayed()) // is a ViewAssertion, isDisplayed() is a ViewMatcher
定位視圖

通常期望的視圖具有唯一性骇吭,但是有的情況下,通過(guò)ViewMater匹配的視圖不唯一歧寺,此時(shí)就會(huì)報(bào)出異常:

android.support.test.espresso.AmbiguousViewMatcherException
此匹配器匹配層次結(jié)構(gòu)中的多個(gè)視圖:( withId:<123456789>)

所以Espresso提供了組合匹配器來(lái)縮小搜索范圍:

onView(allOf(withId(R.id.my_view), withText("Hello!")))

也可以使用反轉(zhuǎn)匹配器

onView(allOf(withId(R.id.test), not(withText("Hello"))));

注意:

  1. 在行為良好的應(yīng)用程序中燥狰,可交互視圖都有描述性文本和內(nèi)容描述文本。如果您無(wú)法使用withText()或 縮小搜索范圍withContentDescription()斜筐,請(qǐng)考慮將其視為可訪問(wèn)性錯(cuò)誤
  2. 通常龙致,一個(gè)視圖都有唯一的標(biāo)示。當(dāng)一個(gè)視圖有唯一的標(biāo)示時(shí)顷链,就不需要再去指定其他的匹配器去匹配目代。
  3. 如果一個(gè)目標(biāo)view是adapterview,例如listview,gridview或者spinner榛了,請(qǐng)使用onData()去獲取數(shù)據(jù)

請(qǐng)參閱ViewMatchers Espresso提供的視圖匹配器在讶。

操作視圖

當(dāng)找到目標(biāo)視圖之后,可以使用perform方法對(duì)其進(jìn)行操作.
例如要單擊視圖:

onView(...).perform(click());

執(zhí)行多個(gè)操作:

onView(...).perform(typeText("hello"), click());

如果目標(biāo)視圖位于scrollview內(nèi)部忽冻,需要考慮顯示視圖之前的操作真朗,比如scrollTo().

onView(...).perform(scrollTo(), click());

請(qǐng)參閱ViewActions查看詳細(xì)API操作。

斷言檢查

可以使用check()將斷言應(yīng)用與當(dāng)前視圖僧诚。最常見的斷言是matches()斷言。

下邊的示例中蝗碎,SimpleActivity包含a Button和a TextView湖笨。單擊按鈕時(shí),內(nèi)容TextView將更改為"Hello Espresso!"蹦骑。

// 匹配視圖button及操作視圖
onView(withId(R.id.button_simple)).perform(click());

// 驗(yàn)證文本
onView(withId(R.id.text_simple)).check(matches(withText("Hello Espresso!")));
檢查AdapterView中的數(shù)據(jù)加載

AdapterView比較特殊慈省,它只能顯示當(dāng)前數(shù)據(jù)的一個(gè)子集的view,所以onView()不會(huì)匹配到那些沒(méi)有顯示的view眠菇。

Espresso單獨(dú)提供了onData()入口來(lái)處理這個(gè)問(wèn)題边败,該入口能夠首先加載所有的item。

下邊是一個(gè)使用 onData()的例子捎废。

// open the item selection
onView(withId(R.id.spinner_simpke)).perform(click());
// select an item
onData(allOf(instanceOf(String.class), is("Americano"))).perform(click());
// verify text correct
onView(withId(R.id.spiinertext_simple)).check(matches(withText(containsString("Americano"))))

Espresso recipes

本篇主要介紹Espresso的進(jìn)階用法笑窜。

匹配另一個(gè)視圖旁邊的視圖
匹配另一個(gè)視圖旁邊的視圖

如上圖所示,如果我們要去匹配7旁邊的item視圖時(shí)登疗,因?yàn)?是非唯一視圖排截,item是唯一視圖,我們可以使用組合匹配器辐益,先去匹配非唯一視圖断傲,然后使用hasSibling(0匹配器縮小選擇范圍。

onView(allOf(withText(7), hasSibling(withText("item: 0")))).perform(click());
匹配Actionbar里邊的視圖

主要是指 ActionBar的控件和Option Item
如果是在actionbar中可見的的視圖智政,是比較簡(jiǎn)單的认罩。

onView(withId(R.id.xxx)).perform(click());

對(duì)于option item來(lái)說(shuō),有的是通過(guò)右上角的menu button調(diào)出的续捂,有的是通過(guò)虛擬菜單鍵調(diào)出的垦垂。幸運(yùn)的是,Espresso為我們處理了額這個(gè)問(wèn)題疾忍。

對(duì)于正常的操作欄:

openActionBarOverflowOrOptionsMenu(getInstrumentation().getTargetContext());
onView(withText("World")).perform(click());

對(duì)于上下文相關(guān)的操作欄

openContexturlActionModeOverflowMenu();
onView(withText("Hello")).perform(click());
斷言view是否顯示
onView(withId(R.id.test)).check(matches(not(isDisplayed())));
斷言view是否存在
onView(withId(R.id.test)).check(doesNotExist());
斷言數(shù)據(jù)項(xiàng)不在適配器中

斷言數(shù)據(jù)想不在適配器中需要我們拿到adapter item乔外,并與數(shù)據(jù)做匹配。這需要我們自己定義方法去匹配一罩。

private static Matcher<View> withAdapterData(Matcher<Object> dataMatcher) {
    @Override
    public void describeTo(Description description){
        description.appendText("with class name: ");
        dataMatcher.describeTo(description);
    }
    
    @Override
    public boolean matchesSafely(View view) {
        if (!(view instanceof AdapterView)) {
            return false;
        }
        
        Adapter adapter = view.getAdapter();
        for (int i = 0; i < adapter.getCount(); i++) {
        if (dataMatch.matches(adapter.getItem(i))){
            return true;
        }
        }
        
        return false;
    }
}

onView(withId(R.id.list)).check(matches(withAdapterData(withItemContent("item 1"))));

注意: 部分代碼詳見AdapterViewTest.java

自定義故障處理程序

espresso允許開發(fā)者自定義FailureHandler用于錯(cuò)誤處理杨幼,常見的做法有收集錯(cuò)誤信息,截圖或者傳遞額外的調(diào)試信息。

private static class CustomFailureHandler implements FailureHandler {
    private final FailureHandler delegate;
    public CustomFailureHandler(Context targetContext) {
        delegate = new DefaultFailureHandler(targetContext);
    }
    
    @Override
    public void handle(Throwable error, Matcher<View> viewMatcher){
        try {
            delegate.handle(error, viewMatcher);
        } catch (NoMatchingViewException e) {
            throw new MySpecialException(e);
        }
    }
}

需要自己定義Exception差购,并且在test中setFailureHandler();一般在setUp()中定義四瘫。

定位非默認(rèn)窗口
onView(withText("south china sea"))
    .inRoot(withDecorView(not(is(getActivity().getWindow().getDecorView()))))
    .perform(click());
在列表視圖中匹配頁(yè)眉和頁(yè)腳

想要匹配頁(yè)眉和頁(yè)腳,必須在listview.addFooter()或者listview.addHeader()中傳遞第二個(gè)參數(shù)欲逃。這個(gè)參數(shù)起到關(guān)聯(lián)作用找蜜。例如:

public static final String FOOTER = "FOOTER";
...
View footerView = layoutInflater.inflate(R.layout.list_item, listView, false);
((TextView) footerView.findViewById(R.id.item_content)).setText("count:");
((TextView) footerView.findViewById(R.id.item_size)).setText(String.valueOf(data.size()));
listView.addFooterView(footerView, FOOTER, true);

測(cè)試代碼:

public static Matcher<Object> isFooter(){
    return allOf(is(instanceOf(String.class)), is(FOOTER));
}

public void testFooter(){
    onData(isFooter()).perform(click());
}

多進(jìn)程

espresso允許跨進(jìn)程測(cè)試,但是只是在Android 8.0及以上版本稳析。所以請(qǐng)注意以下兩點(diǎn):

  • 應(yīng)用最低版本為8.0洗做,也就是API 26。
  • 只能測(cè)試應(yīng)用內(nèi)的進(jìn)程彰居,無(wú)法測(cè)試外部進(jìn)程诚纸。
使用步驟

在build.gradle中引用espresso-remote庫(kù)

dependencies {
...
androidTestImplementation 'com.android.support.test.espresso:espresso-remote:3.0.2'
}

并且需要在androidTest的Manifest文件中添加以下代碼:

<instrumentation android:name="android.support.test.runner.AndroidJUnitRunner"
android:targetPackage="android.support.mytestapp"
android:targetProcesses="xxx"/>

<meta-data
android:name="remoteMethod"
android:value="android.support.test.espresso.remote.EspressoRemote#remoteInit" />

輔助功能檢查

AccessibilityCheck允許使用現(xiàn)有代碼來(lái)測(cè)試可訪問(wèn)性問(wèn)題。它會(huì)在測(cè)試動(dòng)作方法之前發(fā)生陈惰。

import android.support.test.espresso.contrib.AccessibilityChecks;

@RunWith(AndroidJUnit4.class)
@LargeTest
public class AccessibilityChecksIntegrationTest {
    @BeforeClass
    public static void enableAccessibilityChecks() {
        AccessibilityChecks.enable();
    }
}

list

list分為兩種adapterview和recyclerview畦徘。

與adapterview列表項(xiàng)的交互

普通寫法

onData(allOf(is(instanceOf(Map.class)), hasEntry(equalTo("STR"), is("item: 50"))));

注意,Espresso會(huì)根據(jù)需要自動(dòng)滾動(dòng)列表抬闯。

我們分析上邊的代碼井辆,首先is(instanceOf(Map.class))會(huì)將搜索范圍縮小到map集合,然后hasEntry()會(huì)去匹配集合里key為“str”和value為"item: 50"的條目溶握。

因?yàn)樯鲜龃a較長(zhǎng)杯缺,我們可以自定義matcher去match

private static Matcher<Object> withItemContent(String expectedText) {
    checkNotNull(expectedText);
    return withItemContent(equalTo(expectedText));
}

private static Matcher<Object> withItemContent(Matcher<Object> itemTextMatcher) {
    return new BoundedMatcher<>(Map.class){
        @Override
public boolean matchesSafely(Map map) {
    return hasEntry(equalTo("STR"), itemTextMatcher).matches(map);
}

@Override
public void describeTo(Description description) {
    description.appendText("with item content: ");
    itemTextMatcher.describeTo(description);
}
    }
}

這樣就可以簡(jiǎn)單的調(diào)用了

onData(withItemContent("xxx")).perform(click());
操作child view
onData(withItemContent("xxx")).onChildView(withId(R.id.tst)).perform(click());

和recyclerview的條目交互

espresso-contrib庫(kù)中包含一系列recyclerviewactions。

  • scrollTo 滾動(dòng)到匹配的view
  • scrollToHolder 滾動(dòng)到匹配的viewholder
  • scrollToPosition 滾動(dòng)到指定的position
  • actionOnHolderItem 在匹配到的view holder中進(jìn)行操作
  • actionOnItem 在匹配到的item view上進(jìn)行操作
  • actionOnItemAtPosition 在指定位置的view上進(jìn)行操作

實(shí)例:

@Test
public void scrollToItemBelowFold_checkItsText() {
    // First, scroll to the position that needs to be matched and click on it.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition(ITEM_BELOW_THE_FOLD,
            click()));

    // Match the text in an item below the fold and check that it's displayed.
    String itemElementText = mActivityRule.getActivity().getResources()
            .getString(R.string.item_element_text)
            + String.valueOf(ITEM_BELOW_THE_FOLD);
    onView(withText(itemElementText)).check(matches(isDisplayed()));
}
@Test
public void itemInMiddleOfList_hasSpecialText() {
    // First, scroll to the view holder using the isInTheMiddle() matcher.
    onView(ViewMatchers.withId(R.id.recyclerView))
            .perform(RecyclerViewActions.scrollToHolder(isInTheMiddle()));

    // Check that the item has the special text.
    String middleElementText =
            mActivityRule.getActivity().getResources()
            .getString(R.string.middle);
    onView(withText(middleElementText)).check(matches(isDisplayed()));
}

意圖

Espresso-intents庫(kù)是對(duì)espresso的擴(kuò)展奈虾,可以對(duì)被測(cè)應(yīng)用程序發(fā)出的意圖進(jìn)行驗(yàn)證和存根夺谁。

配置
  1. 該功能需要 Android Support Repository

  2. build.gradle的dependencies中添加

     androidTestImplementation 'com.android.support.test.espresso:espresso-intents:3.0.2'
    
  3. add intent test rule

     @Rule
     public IntentsTestRule<MyActivity> intentsTestRule = new IntentTestRule<>(MyActivity.class);
    
  4. match

    Hamcrest的匹配的條件是使用Hamcrest Matchers定義的,Hamcrest允許使用一下兩種意圖匹配器:

    • 使用現(xiàn)有的意圖匹配器
    • 使用自己的意圖匹配器

    Espresso-intents分別提供了意圖驗(yàn)證和存根的方法intented()和intending().

    以下代碼展示了使用啟動(dòng)瀏覽器的意圖匹配的方式:

         intented(allOf(
             hasActoin(equalTo(Intent.ACTION_VIEW)),
             hasCategories(hasItem(qualTo(Intent.CATEGORY_BROWSABLE))),
             hasData(hasHost(equalTo("www.google.com"))),
             hasExtras(allOf(hasEntry(equalTo("key1"), equalTo("value1")),
             toPackage("com.android.browser")))));   
    
  5. validate intents

     intented(toPackage("com.android.phone"));
    
  6. stubbing

     1. 構(gòu)建結(jié)果以在啟動(dòng)特定活動(dòng)時(shí)返回肉微。
     2. 指示存根結(jié)果以響應(yīng)意圖
     3. 驗(yàn)證是否產(chǎn)生了預(yù)期的目標(biāo)
     
     完整代碼如下:
         
         // Build the result to return when the activity is launched.
         Intent resultData = new Intent();
         String phoneNumber = "123-345-6789";
         resultData.putExtra("phone", phoneNumber);
         ActivityResult result =
             new ActivityResult(Activity.RESULT_OK, resultData);
    
         // Set up result stubbing when an intent sent to "contacts" is seen.
         intending(toPackage("com.android.contacts")).respondWith(result);
    
         // User action that results in "contacts" activity being launched.
         // Launching activity expects phoneNumber to be returned and displayed.
         onView(withId(R.id.pickButton)).perform(click());
    
         // Assert that the data we set up above is shown.
         onView(withId(R.id.phoneNumber)).check(matches(withText(phoneNumber)));
    

webview


Espresso Web

應(yīng)該用不到吧匾鸥,跳過(guò)了

idling resource

閑置資源

總結(jié)

Espresso小結(jié)備忘.pdf

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市碉纳,隨后出現(xiàn)的幾起案子勿负,更是在濱河造成了極大的恐慌,老刑警劉巖劳曹,帶你破解...
    沈念sama閱讀 217,542評(píng)論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奴愉,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡铁孵,警方通過(guò)查閱死者的電腦和手機(jī)锭硼,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)蜕劝,“玉大人檀头,你說(shuō)我怎么就攤上這事轰异。” “怎么了暑始?”我有些...
    開封第一講書人閱讀 163,912評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵搭独,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我廊镜,道長(zhǎng)牙肝,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評(píng)論 1 293
  • 正文 為了忘掉前任嗤朴,我火速辦了婚禮配椭,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘雹姊。我一直安慰自己颂郎,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評(píng)論 6 392
  • 文/花漫 我一把揭開白布容为。 她就那樣靜靜地躺著,像睡著了一般寺酪。 火紅的嫁衣襯著肌膚如雪坎背。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,370評(píng)論 1 302
  • 那天寄雀,我揣著相機(jī)與錄音得滤,去河邊找鬼。 笑死盒犹,一個(gè)胖子當(dāng)著我的面吹牛懂更,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播急膀,決...
    沈念sama閱讀 40,193評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼沮协,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了卓嫂?” 一聲冷哼從身側(cè)響起慷暂,我...
    開封第一講書人閱讀 39,074評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎晨雳,沒(méi)想到半個(gè)月后行瑞,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡餐禁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評(píng)論 3 335
  • 正文 我和宋清朗相戀三年血久,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片帮非。...
    茶點(diǎn)故事閱讀 39,841評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氧吐,死狀恐怖讹蘑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情副砍,我是刑警寧澤衔肢,帶...
    沈念sama閱讀 35,569評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站豁翎,受9級(jí)特大地震影響角骤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜心剥,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評(píng)論 3 328
  • 文/蒙蒙 一邦尊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧优烧,春花似錦蝉揍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至熙卡,卻和暖如春杖刷,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背驳癌。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工滑燃, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人颓鲜。 一個(gè)月前我還...
    沈念sama閱讀 47,962評(píng)論 2 370
  • 正文 我出身青樓表窘,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親甜滨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子乐严,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評(píng)論 2 354

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