[譯]利用 Espresso 和 Dagger 編寫可靠的功能測試

原文:Reliable functional tests with Espresso and Dagger
作者:Egor Andreevici
譯者:lovexiaov

可靠性是自動化測試的一個核心要素松靡,這意味著無論執(zhí)行多少次,無論在什么情況下執(zhí)行,它的結(jié)果應(yīng)該一致布近,都通過或都失敗举塔。有些測試在某些時候會由于未知原因?qū)е陆Y(jié)果失敗僚稿,這類測試被稱為不可靠的矿咕,這是一個真實存在的問題沪伙。有時開發(fā)團隊會直接放棄一遍又一遍的修復(fù)此類不可靠問題名挥,他們會跳過執(zhí)行該測試疟羹。這樣,我們將不能免于執(zhí)行回歸測試禀倔。單元測試通常會通過模擬所有依賴避免出現(xiàn)此類情況榄融,而功能測試有自己的實現(xiàn)方式。一個經(jīng)典的例子是在屏幕上加載從網(wǎng)絡(luò)上獲取的數(shù)據(jù)——在離線狀態(tài)下救湖,每次執(zhí)行測試都會失斃⒈!那么鞋既,我們要如何編寫可靠的功能測試而不受網(wǎng)絡(luò)狀況的影響呢力九?本文我將介紹一種使用 Dagger 創(chuàng)建簡潔且健壯的功能測試的方法耍铜。

什么是 Dagger

Dagger 已經(jīng)成為眾多 Android 開發(fā)者軍火庫中的必備工具,如果你還沒聽說過它——它是一個快速的依賴注入框架跌前,由 Square 開發(fā)棕兼,并針對 Android 做了特別優(yōu)化。不像其他流行的依賴注入器抵乓,Dagger 沒有使用反射伴挚,而是依靠生成代碼提高執(zhí)行速度。我們將在應(yīng)用中使用 Dagger 用一種簡潔的方法替代依賴灾炭,沒有破壞代碼封裝茎芋,也不會寫多余的只用于測試的代碼。還等什么呢咆贬!

天氣應(yīng)用

我們將會開發(fā)一個簡單的只有一個界面的天氣應(yīng)用來作為演示败徊。此應(yīng)用請求用戶提供城市名稱,然后下載該城市當(dāng)前天氣的信息掏缎。如下所示:

weather.png

完整的源碼托管在 GitHub 上皱蹦。

OpenWeatherMap API

我們將會使用OpenWeatherMap API 來獲取天氣數(shù)據(jù)。此 API 是免費的眷蜈,但是如果你想要在自己機器上下載并編譯應(yīng)用沪哺,你需要注冊來獲取一個 API key。

設(shè)置 REST API client

下面我們來設(shè)置 REST API client 實現(xiàn)獲取數(shù)據(jù)功能酌儒。我們將會使用 Retrofit 配合 RxJava完成實現(xiàn)辜妓,所以需要將以下依賴加入到 build.gradle 中:

dependencies {  
    // rest of dependencies

    compile 'com.squareup.retrofit:retrofit:1.9.0'
    compile 'io.reactivex:rxandroid:1.0.1'
}

接下來是一個簡單的名為 WeatherData 的 POJO,該類將代表我們從服務(wù)器上獲取的信息忌怎。

public class WeatherData {

    public static final String DATE_FORMAT = "EEEE, d MMM";

    private static final int KELVIN_ZERO = 273;

    private static final String FORMAT_TEMPERATURE_CELSIUS = "%d°";
    private static final String FORMAT_HUMIDITY = "%d%%";

    private String name;
    private Weather[] weather;
    private Main main;

    public String getCityName() {
        return name;
    }

    public String getWeatherDate() {
        return new SimpleDateFormat(DATE_FORMAT, Locale.getDefault()).format(new Date());
    }

    public String getWeatherState() {
        return weather().main;
    }

    public String getWeatherDescription() {
        return weather().description;
    }

    public String getTemperatureCelsius() {
        return String.format(FORMAT_TEMPERATURE_CELSIUS, (int) main.temp - KELVIN_ZERO);
    }

    public String getHumidity() {
        return String.format(FORMAT_HUMIDITY, main.humidity);
    }

    private Weather weather() {
        return weather[0];
    }

    private static class Weather {
        private String main;
        private String description;
    }

    private static class Main {
        private float temp;
        private int humidity;
    }
}

然后是簡單的 Retrofit 接口籍滴,該接口包含了我們用來獲取數(shù)據(jù)的 GET 請求的描述:

public interface WeatherApiClient {

    Endpoint ENDPOINT = Endpoints.newFixedEndpoint("http://api.openweathermap.org/data/2.5");

    @GET("/weather") Observable<WeatherData> getWeatherForCity(@Query("q") String cityName);
}

以上是針對網(wǎng)絡(luò)的設(shè)置。下面讓我們來配置 Dagger 使它能提供一個 WeatherApiClient 類的實現(xiàn)供需要的類調(diào)用榴啸。

配置 Dagger

build.gradle 文件中添加以下幾行將 Dagger 配置到你的工程中:

final DAGGER_VERSION = '2.0.2'

dependencies {  
    // Retrofit dependencies are here

    compile "com.google.dagger:dagger:${DAGGER_VERSION}"
    apt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

你可能注意到了我們在 apt 作用域中引入了 dagger-compiler:因為 dagger-compiler 是一個注解處理器孽惰,我們只希望在編譯時期使用它而不想將它打包到 APK 中(就 dex 方法數(shù)限制而言 dagger-compiler 是十分龐大的)∨赣。可以使用 android-apt 插件來實現(xiàn)此功能勋功。將以下行添加到應(yīng)用要目錄的 build.gradle 文件中:

buildscript {  
    dependencies {
        // other classpath declarations
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然后在 app 目錄下的 build.gradle 文件的最上方添加一行:

apply plugin: 'com.neenbedankt.android-apt'

現(xiàn)在,我們得到了所有需要的依賴库说。下面我們會創(chuàng)建一個 Dagger 模塊狂鞋,該模塊描述了我們提供依賴的邏輯:

@Module
public class AppModule {

    private final Context context;

    public AppModule(Context context) {
        this.context = context.getApplicationContext();
    }

    @Provides @AppScope public Context provideAppContext() {
        return context;
    }

    @Provides public WeatherApiClient provideWeatherApiClient() {
        return new RestAdapter.Builder()
                .setEndpoint(WeatherApiClient.ENDPOINT)
                .setRequestInterceptor(apiKeyRequestInterceptor())
                .setLogLevel(BuildConfig.DEBUG ? RestAdapter.LogLevel.FULL : RestAdapter.LogLevel.NONE)
                .build()
                .create(WeatherApiClient.class);
    }

    private RequestInterceptor apiKeyRequestInterceptor() {
        return new ApiKeyRequestInterceptor(context.getString(R.string.open_weather_api_key));
    }
}

如你所見,provideWeatherApiClient() 真實的創(chuàng)建了 WeatherApiClient的實例潜的,并將其返回:每次我們請求它提供一個 WeatherApiClient實例時骚揍,這段代碼都會被調(diào)用。太爽啦啰挪!現(xiàn)在我們添加一個 Component 接口疏咐,該接口描述了 Dagger 創(chuàng)建的我們程序依賴關(guān)系圖的約定:

@AppScope
@Component(modules = AppModule.class)
public interface AppComponent {

    void inject(MainActivity activity);

    @AppScope Context appContext();

    WeatherApiClient weatherApiClient();
}

AppComponent 能夠提供應(yīng)用 Context 的實例以及 WeatehrApiClient 的實例纤掸,它還可以向 MainActivity 中注入依賴。

最后浑塞,我們需要實例化 AppComponent 并使它可被其他類使用。我們會將以下代碼加入到自定義的 ApplicationWeatherApp 中:

public class WeatherApp extends Application {

    private AppComponent appComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        appComponent = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .build();
    }

    public AppComponent appComponent() {
        return appComponent;
    }
}

現(xiàn)在我們可以打開 MainActivity看一下我們?nèi)绾问褂?WeatherApiClient 獲取 天氣數(shù)據(jù)的政己。

MainActivity

MainActivity 中相關(guān)代碼(完整代碼):

public class MainActivity extends AppCompatActivity implements SearchView.OnQueryTextListener {

    @Inject WeatherApiClient weatherApiClient;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((WeatherApp) getApplication()).appComponent().inject(this);
    }

    @Override
    public boolean onQueryTextSubmit(String query) {
        if (!TextUtils.isEmpty(query)) {
            loadWeatherData(query);
        }
        return true;
    }

    private void loadWeatherData(String cityName) {
        subscription = weatherApiClient.getWeatherForCity(cityName)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(
                        // handle result
                        }
                );
    }
}

請注意看我們?nèi)绾螌嵗?WeatherApiClient 的:我們沒有手動創(chuàng)建酌壕,而是使用注解 @Inject 標(biāo)記,并在 onCreate() 中做如下操作:

((WeatherApp) getApplication()).appComponent().inject(this);

通過訪問我們的 AppComponent 并將它注入到 MainActivity 中歇由, 我們使 Dagger 滿足了所有的依賴需求(通過使用 @Inject 標(biāo)記卵牍,它出色的完成的任務(wù))。接下來我們就可以使用 WeatherApiClient 獲取數(shù)據(jù)了沦泌。

盡管此方式乍看起來啰嗦且不簡明糊昙,它真正強大的地方在于我們不需要硬編碼創(chuàng)建依賴。這種優(yōu)勢將在下一步我們需要替代測試代碼中的依賴時突顯出來谢谦。

配置 Espresso

現(xiàn)在讓我們將 Espresso 集成到工程中释牺,并編寫測試驗證我們能否正常獲取數(shù)據(jù)并展示數(shù)據(jù)。首先回挽,添加以下依賴到 build.gradle 中:

final ESPRESSO_VERSION = '2.2.1'  
final ESPRESSO_RUNNER_VERSION = '0.4'

dependencies {  
    // 'compile' dependencies

    androidTestCompile "com.android.support.test:runner:${ESPRESSO_RUNNER_VERSION}"
    androidTestCompile "com.android.support.test:rules:${ESPRESSO_RUNNER_VERSION}"
    androidTestCompile "com.android.support.test.espresso:espresso-core:${ESPRESSO_VERSION}"
    androidTestApt "com.google.dagger:dagger-compiler:${DAGGER_VERSION}"
}

這里我們也用到了 dagger-compiler没咙,因為我們的測試代碼也必須使用注解處理器執(zhí)行。接下來我們添加一個測試類:

@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    private static final String CITY_NAME = "München";

    @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Inject WeatherApiClient weatherApiClient;

    @Before
    public void setUp() {
        weatherApiClient = ((WeatherApp) activityTestRule.getActivity().getApplication()).appComponent()
                .weatherApiClient();
    }

    @Test
    public void correctWeatherDataDisplayed() {
        WeatherData weatherData = weatherApiClient.getWeatherForCity(CITY_NAME).toBlocking().first();

        onView(withId(R.id.action_search)).perform(click());
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(replaceText(CITY_NAME));
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(pressKey(KeyEvent.KEYCODE_ENTER));

        onView(withId(R.id.city_name)).check(matches(withText(weatherData.getCityName())));
        onView(withId(R.id.weather_date)).check(matches(withText(weatherData.getWeatherDate())));
        onView(withId(R.id.weather_state)).check(matches(withText(weatherData.getWeatherState())));
        onView(withId(R.id.weather_description)).check(matches(withText(weatherData.getWeatherDescription())));
        onView(withId(R.id.temperature)).check(matches(withText(weatherData.getTemperatureCelsius())));
        onView(withId(R.id.humidity)).check(matches(withText(weatherData.getHumidity())));
    }
}

測試用例簡潔明了:我們想我為指定城市加載天氣數(shù)據(jù)并驗證數(shù)據(jù)是否正常顯示千劈。這在多數(shù)情況下應(yīng)該都是正常的祭刚,但想象以下如果在飛行模式下執(zhí)行呢?很可能會失斍脚啤涡驮!由于我們設(shè)計的測試用例時用來驗證應(yīng)用是否能正常顯示數(shù)據(jù),而不能聯(lián)網(wǎng)導(dǎo)致的數(shù)據(jù)缺失不是有效場景喜滨,該場景會使我們的測試失敗捉捅。另外,我們可能會編寫另一個測試用例來檢查在飛行模式下應(yīng)用的行為是否正澈枋校——如何使這兩個測試用例同時執(zhí)行通過呢锯梁?Dagger 可以搞定!讓我們利用依賴注入的力量焰情,提供一個可配置我們期望接收數(shù)據(jù)的 WeatherApiClient 的實現(xiàn)陌凳。

MockWeatherApiClient

我們的一個解決方案是一個返回硬編碼數(shù)據(jù)的 WeatherApiClient。創(chuàng)建 TestData 類内舟,該類中存放了我們期望返回的 JSON 數(shù)據(jù)合敦。

public final class TestData {

    public static final String MUNICH_WEATHER_DATA_JSON = "\n" +
            "{\n" +
            "    \"coord\": {\n" +
            "        \"lon\": 11.58,\n" +
            "        \"lat\": 48.14\n" +
            "    },\n" +
            "    \"weather\": [{\n" +
            "        \"id\": 741,\n" +
            "        \"main\": \"Fog\",\n" +
            "        \"description\": \"fog\",\n" +
            "        \"icon\": \"50n\"\n" +
            "    }],\n" +
            "    \"base\": \"cmc stations\",\n" +
            "    \"main\": {\n" +
            "        \"temp\": 275.68,\n" +
            "        \"pressure\": 1030,\n" +
            "        \"humidity\": 93,\n" +
            "        \"temp_min\": 274.15,\n" +
            "        \"temp_max\": 277.15\n" +
            "    },\n" +
            "    \"wind\": {\n" +
            "        \"speed\": 1.5,\n" +
            "        \"deg\": 240\n" +
            "    },\n" +
            "    \"clouds\": {\n" +
            "        \"all\": 0\n" +
            "    },\n" +
            "    \"dt\": 1449350400,\n" +
            "    \"sys\": {\n" +
            "        \"type\": 1,\n" +
            "        \"id\": 4887,\n" +
            "        \"message\": 0.0134,\n" +
            "        \"country\": \"DE\",\n" +
            "        \"sunrise\": 1449298092,\n" +
            "        \"sunset\": 1449328836\n" +
            "    },\n" +
            "    \"id\": 6940463,\n" +
            "    \"name\": \"Altstadt\",\n" +
            "    \"cod\": 200\n" +
            "}";

    private TestData() {
        // no instances
    }
}

MockWeatherApiClient 只需要解析返回的 JSON 數(shù)據(jù)。我們還可以加入延遲以模仿網(wǎng)絡(luò)延遲:

public class MockWeatherApiClient implements WeatherApiClient {

    @Override public Observable<WeatherData> getWeatherForCity(String cityName) {
        WeatherData weatherData = new Gson().fromJson(TestData.MUNICH_WEATHER_DATA_JSON, WeatherData.class);
        return Observable.just(weatherData).delay(1, TimeUnit.SECONDS);
    }
}

有了可配置的 WeatherApiClient验游,我們不在需要依賴任何的外部狀況充岛,我們可以配置它來返回任何我們想要測試的數(shù)據(jù)保檐。接下來,我們將找出使 MockWeatherApiClient 可用的方法崔梗。

配置 Dagger 測試

我們需要模仿在我們應(yīng)用代碼中的配置步驟夜只,從創(chuàng)建 TestAppModule 類開始:

@Module
public class TestAppModule {

    private final Context context;

    public TestAppModule(Context context) {
        this.context = context.getApplicationContext();
    }

    @Provides @AppScope public Context provideAppContext() {
        return context;
    }

    @Provides public WeatherApiClient provideWeatherApiClient() {
        return new MockWeatherApiClient();
    }
}

該類與 AppMoudle 十分相似,但是我們沒有使用 Retrofit 創(chuàng)建真是的 WeatherApiClient 的實現(xiàn)蒜魄,而是簡單的實例化了 MockWeatherApiClient扔亥。接下來添加 TestAppComponent

@AppScope
@Component(modules = TestAppModule.class)
public interface TestAppComponent extends AppComponent {

    void inject(MainActivityTest test);
}

TestAppComponent 繼承了 AppComonent 并添加了我們測試類需要的 inject() 方法。接下來修改測試類的 setUp() 方法:

@Before
public void setUp() {  
    ((TestWeatherApp) activityTestRule.getActivity().getApplication()).appComponent().inject(this);
}

最后谈为,我們使用測試替身替換 WeatherApp

public class TestWeatherApp extends WeatherApp {

    private TestAppComponent testAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();

        testAppComponent = DaggerTestAppComponent.builder()
                .testAppModule(new TestAppModule(this))
                .build();
    }

    @Override
    public TestAppComponent appComponent() {
        return testAppComponent;
    }
}

注意我們這里返回的是 TestAppComponent 而不是 AppComponent旅挤。 類的接口保持不變,這意味著使用測試替身對應(yīng)用代碼毫無影響伞鲫。

我們現(xiàn)在配置完了 Dagger粘茄,但還遺漏了關(guān)鍵的一點:如何讓我們的測試使用 TestWeatherApp 而不是 WeatherApp?答案是使用自定義測試執(zhí)行器秕脓!

實現(xiàn)自定義測試執(zhí)行器

用來執(zhí)行 Espresso 測試的 AndroidJUnitRunner 有一個便捷的方法 newApplication()柒瓣,我們可以覆寫該方法來使用 TestWeatherApp 替換 WeatherApp

public class WeatherTestRunner extends AndroidJUnitRunner {

    @Override
    public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String testApplicationClassName = TestWeatherApp.class.getCanonicalName();
        return super.newApplication(cl, testApplicationClassName, context);
    }
}

不要忘記在 build.gradle 中聲明它喲:

defaultConfig {  
    // rest of configuration

    testInstrumentationRunner "me.egorand.weather.runner.WeatherTestRunner"
}

這樣就可以了!我們可以使用以下命令執(zhí)行測試:

./gradlew connectedAndroidTest

至此撒会,我們已經(jīng)完成不受網(wǎng)絡(luò)影響執(zhí)行功能測試的配置嘹朗,并保證了測試結(jié)果正常執(zhí)行。請到 GitHub` 上查看本文用到的完整源碼诵肛。

結(jié)論

正如我在使用 Espresso 測試一個有序列表(中譯版)中所說屹培,有一套驗收測試是一種 catch regressions 的很好方式,并且保證了絕大多數(shù)的 bug 會被開發(fā)團隊發(fā)現(xiàn)怔檩,而不是終端用戶褪秀。那么保證你測試的可靠性就變得十分重要:不可靠的測試只會浪費團隊的時間去一遍一遍的修復(fù)它們,直到所有人都決定不去執(zhí)行這些測試薛训。

通過使用 Dagger 我們可以使代碼與依賴注入邏輯解耦媒吗,這將允許我們使用測試替身并且控制待測應(yīng)用的某些方面。本文描述了使用此技術(shù)允許在離線模式下執(zhí)行網(wǎng)絡(luò)相關(guān)的測試乙埃,并保證它們正常執(zhí)行闸英。值得一提的是,此方法不適用于端對端的測試介袜,因為我們沒有像用戶一樣在真實環(huán)境中測試應(yīng)用甫何。然而,這仍然是一個非常有效的執(zhí)行功能測試的方法遇伞,也使你能很靈活的測試應(yīng)用各方面邏輯辙喂。

你有在自己的 Espresso 中使用 Dagger 嗎?希望能與你交流。如果你有任何反饋或發(fā)現(xiàn)文中錯誤巍耗,歡迎留言或直接與我聯(lián)系秋麸,祝好!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末炬太,一起剝皮案震驚了整個濱河市灸蟆,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌亲族,老刑警劉巖次乓,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異孽水,居然都是意外死亡,警方通過查閱死者的電腦和手機城看,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門女气,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人测柠,你說我怎么就攤上這事炼鞠。” “怎么了轰胁?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵谒主,是天一觀的道長。 經(jīng)常有香客問我赃阀,道長霎肯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任榛斯,我火速辦了婚禮观游,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘驮俗。我一直安慰自己懂缕,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布王凑。 她就那樣靜靜地躺著搪柑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪索烹。 梳的紋絲不亂的頭發(fā)上工碾,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天,我揣著相機與錄音术荤,去河邊找鬼倚喂。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的端圈。 我是一名探鬼主播焦读,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼舱权!你這毒婦竟也來了矗晃?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤宴倍,失蹤者是張志新(化名)和其女友劉穎张症,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸵贬,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡俗他,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了阔逼。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兆衅。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖嗜浮,靈堂內(nèi)的尸體忽然破棺而出羡亩,到底是詐尸還是另有隱情,我是刑警寧澤危融,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布畏铆,位于F島的核電站,受9級特大地震影響吉殃,放射性物質(zhì)發(fā)生泄漏辞居。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一寨腔、第九天 我趴在偏房一處隱蔽的房頂上張望速侈。 院中可真熱鬧,春花似錦迫卢、人聲如沸倚搬。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽每界。三九已至,卻和暖如春家卖,著一層夾襖步出監(jiān)牢的瞬間眨层,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工上荡, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留趴樱,地道東北人馒闷。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像叁征,于是被迫代替她去往敵國和親纳账。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,747評論 2 361

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