IT類翻譯 -- 用NSURProtocol注入測試數(shù)據(jù)

注:文章翻譯自網(wǎng)絡(luò)博客驻子。

Using NSURLProtocol forInjecting Test Data

September13, 2011 By: Claus Broch Filed in: Develop | ILBitly | iPhone | Testing |Tutorial

NSURProtocol注入測試數(shù)據(jù)

2011年9月13日,Claus Broch分類:Develop| ILBitly | iPhone | Testing | Tutorial

Inearlier posts I described methods for unit testing asynchronous network accessand how to use mock objects for further control of the scope of these unittests. In this tutorial I’ll present an alternative way of providing reliabletest data by customizing the NSURLProtocol class in order to deliver statictest data.

在之前的幾篇博文中,我介紹過訪問異步網(wǎng)絡(luò)的單元測試方法及如何用mock對象進(jìn)一步確定單元測試的范圍剃诅。今天的教程中正林,我將展示另一種方法赡模,即:通過自定義NSURProtocol類來獲取靜態(tài)測試數(shù)據(jù)婿着,從而為測試可靠的數(shù)據(jù)。

Afew months ago Gowalla made the networking code used in their iPhone clientavailable as open source on GitHub. The AFNetworking library as it is called isa “A delightful iOS networking library with NSOperations and block-basedcallbacks“. One of the things that first caught my eye was the built-in supportfor accessing JSON based services with just a few lines of code.

幾個月前Gowalla在GitHub上公開了他們用于iPhone客戶端的網(wǎng)絡(luò)代碼近她。這個被稱為AFNetworking的庫叉瘩,是一個“使用NSOperations和block回調(diào)的、討喜的iOS網(wǎng)絡(luò)庫”粘捎。這段代碼中首先吸引我的一個部分是:利用該庫內(nèi)置的支持服務(wù)薇缅,僅需幾行代碼即可訪問JSON服務(wù)。

Thesimplicity of the AFNetworking interface inspired me to give it a test spin andwrite ILBitly which provides an Objective C based wrapper for the Bitly urlshortening service. It’s very easy to use AFNetworking and especially the JSONsupport that is accessed using a single class methods. Unfortunately thissimplicity also makes it quite difficult to write self-contained unit and mocktests using OCMock. This is mainly because OCMock doesn’t support mocking ofclass methods. My attempts with other techniques such as method swizzlingwasn’t successful either.

AFNetworking的界面之簡潔,啟發(fā)我運(yùn)行一次快速的測試,并編寫了ILBitly仇参。ILBitly可提供一個基于Object C的包裝類,從而獲得Bitly的URL縮短服務(wù)蓬痒。AFNetworking的使用非常簡單,尤其是JSON的支持服務(wù)漆羔,僅需調(diào)用一個單個類的方法即可獲得梧奢。然而這簡潔性也為我們使用MCMock編寫自包含單元和mock測試增添不少難度。這主要因?yàn)镺CMock不支持mocking類方法演痒。我也嘗試過其它方法亲轨,例如method swizzling,然而也并不成功鸟顺。

Itwasn’t until a few days ago when I noticed a discussion on GitHub about how toproperly mock the interface to AFNetworking. In the discussion Adam Ernstsuggested to use a customized NSURLProtocol for doing the task. That finallygave me the missing clue on how to solve the testing problem.

就在幾天前惦蚊,我才看到GitHub上的一則討論,有關(guān)如何恰當(dāng)?shù)乩肁FNetworking的接口讯嫂,引入mock對象蹦锋。討論中Adam

Ernst建議使用自定義的NSURLProtocol來完成這項(xiàng)任務(wù)。這讓我靈光一現(xiàn)欧芽,終于想到了該如何解決我的測試問題莉掂。

Subclassing NSURLProtocol

子類化NSURLProtocol

Asmentioned above I didn’t find any easy way to mock the interface toAFJSONRequestOperation in order to intercept the network access. So analternative solution is to intercept the standard http protocol built into iOS.This is done by registering our own custom NSURLProtocol subclass capable ofhandling http requests: ILCannedURLProtocol. Since each registered protocolhandler is asked in reverse order of registration our class will always beconsulted before the standard classes.

如上文提到的,我需要攔截網(wǎng)絡(luò)訪問千扔,但我當(dāng)時找不到一種簡單的方法來模擬AFJSONRequestOperation的接口憎妙。于是我想到了另一條路库正,即攔截iOS內(nèi)置的標(biāo)準(zhǔn)http協(xié)議。這可以通過注冊我們自定義的NSURLProtocol子類ILCannedURLProtocol來實(shí)現(xiàn)厘唾。該子類可處理http請求褥符。由于詢問protocol

handler的順序,與注冊順序是相反的抚垃。因此相較于標(biāo)準(zhǔn)類喷楣,我們的類總是會被優(yōu)先訪問。

Theprimary goal of ILCannedURLProtocol is to respond with a pre-loaded set of testdata every time a http request is made. This way we’ll be able to remove anyoutside influences when running the tests. We’ll also be able to have the httprequest fail when we want it to fail. The interface for ILCannedURLProtocol is shownbelow:

這樣做的主要目的是讯柔,每當(dāng)出現(xiàn)一個http請求抡蛙,ILCannedURLProtocol即會回應(yīng)一組預(yù)先加載好的測試數(shù)據(jù)护昧。如此一來魂迄,我們就能在測試中消除所有外部影響。同時惋耙,我們可以在需要時捣炬,故意使http的請求失敗。ILCannedURLProtocol的接口如下所示:

@interfaceILCannedURLProtocol : NSURLProtocol

+(void)setCannedResponseData:(NSData*)data;

+(void)setCannedHeaders:(NSDictionary*)headers;

+(void)setCannedStatusCode:(NSInteger)statusCode;

+(void)setCannedError:(NSError*)error;

@end

Itis not able to fully replace any http requests in its current form. Forinstance it is only designed to intercept GET requests. Neither does it supportany type of authentication challenge/response. But it provides enoughfunctionality to deliver the test data needed for testing ILBitly and probablyother similar classes.

在現(xiàn)有http請求的形式下绽榛,我們并不能替換掉任何一個請求的全部內(nèi)容湿酸。舉例來說,我們只能攔截GET請求灭美,卻無法攔截任何類型的權(quán)限認(rèn)證質(zhì)詢(authentication

challenge)或認(rèn)證應(yīng)答(authentication response)推溃。但它現(xiàn)有的功能已經(jīng)足以為測試ILBitly及其相似的類提供測試數(shù)據(jù).

Basicallyeach of the setCannedXxx methods just retains the object passed to it so theobject can be returned again when needed by a http request. This also meansthat it is only able to serve one set of test data at a time.

基本上每個setCannedXxx方法都會保留下傳給它的對象,因此每當(dāng)http請求需要時届腐,可以返回這些對象铁坎。但這也意味著它們只能每次應(yīng)對一組測試數(shù)據(jù)。

Thereare a few additional methods that need to be implemented when subclassingNSURLProtocol. One of these iscanInitWithRequest: This method is called every time a NSURLRequest is startedin order to determine if that request is supported by the class. We’ll use thatto intercept the http GET requests:

子類化NSURLProtocol還需要實(shí)現(xiàn)一些其他的方法犁苏。其中之一是canInitWithRequest:每當(dāng)發(fā)起一個NSURLRequest時硬萍,都會調(diào)用該方法,來判斷該類是否支持這一請求围详。我們將使用這個方法來攔截http GET請求:

+(BOOL)canInitWithRequest:(NSURLRequest *)request {

// For now only supporting http GET

return [[[request URL] scheme]isEqualToString:@"http"]

&& [[request HTTPMethod]isEqualToString:@"GET"];

}

Wealso need to implement the startLoading method. This method is called once theappropriate protocol handler has been instantiated in order to service therequest with data. Our method is able to either respond with a successfulresponse or with an error depending on which of the canned data that has beenset:

同時我們也需要實(shí)現(xiàn)startLoading方法朴乖。該方法會在每次實(shí)例化相關(guān)protocol handler時被調(diào)用,從而給請求提供數(shù)據(jù)助赞。根據(jù)設(shè)置的封裝數(shù)據(jù)不同买羞,我們的方法將會給出一個成功的回應(yīng),或者報(bào)出一個錯誤:

-(void)startLoading {

NSURLRequest *request = [self request];

id client = [self client];

if(gILCannedResponseData) {

// Send the canned data

NSHTTPURLResponse *response =

[[NSHTTPURLResponse alloc]initWithURL:[request URL]

statusCode:gILCannedStatusCode

headerFields:gILCannedHeaders

requestTime:0.0];

[client URLProtocol:selfdidReceiveResponse:response

cacheStoragePolicy:NSURLCacheStorageNotAllowed];

[client URLProtocol:selfdidLoadData:gILCannedResponseData];

[client URLProtocolDidFinishLoading:self];

[response release];

}

else if(gILCannedError) {

// Send the canned error

[client URLProtocol:selfdidFailWithError:gILCannedError];

}

}

Ifyou decide to use the above code for testing in your own project you must makesure not to accidentally include it in the production code for any appstargeted for the App Store. If you haven’t already spotted the reason for thisI’ll lead your attention to the initializer for NSHTTPURLResponse. This is aprivate api obtained by running class-dump on the iOS 4.3 SDK. If you includethis call in your production code you therefore risk it being rejected byApple. There is also a slight chance Apple might decide to modify it in futureupdates of iOS. But as long as it’s just used when running the unit testseverything should be fine.

如果你決定在自己的項(xiàng)目中使用上述代碼測試雹食,小心不要把它寫入任何打算上傳到APP Store的產(chǎn)品代碼中去畜普。如果你不明白為什么,讓我們來看一下NSHTTPURLResponse的初始化程序婉徘。這是一個私有API漠嵌,通過在iOS 4.3 SDK上運(yùn)行class-dump來獲取咐汞。如果你把這段回調(diào)加在你的產(chǎn)品代碼中,蘋果就又能會拒了它儒鹿。蘋果甚至可能會在未來的iOS更新中對它進(jìn)行修改化撕,盡管可能性不大。但如果只是用它來跑單元測試的話约炎,那應(yīng)該沒什么問題植阴。

Exceptfor a few other methods which are basically empty that’s all there is to it.Now we’ll just need to register our custom class and load some canned data intoit.

除去另外幾個基本為空的方法,所有的方法都在這了』常現(xiàn)在我們只需注冊我們自定義的類掠手,然后再加載一些封裝數(shù)據(jù)進(jìn)去。

Preparing the Unit Tests

準(zhǔn)備單元測試

Theunit test class for ILBitly just includes a few instance variables:

ILBitly的單元測試類只包含了幾個實(shí)例變量:

@interfaceILBitlyTest : SenTestCase {

ILBitly *bitly;

id bitlyMock;

BOOL done;

}

@end

Thebitly variable contains an instance of the ILBitly code under test, bitlyMockholds the partial mock object for the ILBitly test, and done is used forsignaling when the asynchronous calls have finished. These are explained morein details later.

變量bitly包含test下ILBitly代碼的一個實(shí)例狸捕,bitlyMock包含了用作ILBitly測試的部分mock對象喷鸽,done是異步調(diào)用結(jié)束的信號。后面我會詳細(xì)地解釋這些變量灸拍。

Beforeevery test case is executed the setUp method is automatically called allowingus to prepare things:

執(zhí)行每個測試用例之前做祝,setUp方法都會被自動調(diào)用,來做以下準(zhǔn)備:

-(void)setUp

{

[super setUp];

// Init bitly proxy using test id and key -not valid for real use

bitly = [[ILBitly alloc]initWithLogin:@"LOGIN" apiKey:@"KEY"];

done = NO;

[NSURLProtocol registerClass:[ILCannedURLProtocolclass]];

[ILCannedURLProtocolsetCannedStatusCode:200];

}

We’lluse this method to prepare a default test instance as well as registering theILCannedURLProtocol. The parameters used for initializing the ILBitly instanceare just placeholders which are passed on to the service requests. Since we’llbe using static test data they have no real meaning except that we’ll verifylater on that they are actually passed on as expected.

我們這個方法來準(zhǔn)備默認(rèn)的測試實(shí)例鸡岗,以及注冊ILCannedURLProtocol混槐。那些用來實(shí)例化ILBitly的參數(shù)只是傳給服務(wù)請求的占位符。因?yàn)橹笪覀儠褂渺o態(tài)測試數(shù)據(jù)轩性,所以它們其實(shí)并沒有什么實(shí)際的用途声登,僅供我們稍后確認(rèn)它們是否被如期傳遞了。

Inorder to balance things out properly, we’ll unregister our custom protocol aswell as dispose of the test data after each test:

為了平衡資源揣苏,每次測試后悯嗓,我們都會注銷自定義協(xié)議,同時銷毀測試數(shù)據(jù)舒岸。

-(void)tearDown

{

[NSURLProtocolunregisterClass:[ILCannedURLProtocol class]];

[ILCannedURLProtocol setCannedHeaders:nil];

[ILCannedURLProtocolsetCannedResponseData:nil];

[ILCannedURLProtocol setCannedError:nil];

[bitly release];

bitlyMock = nil;

[super tearDown];

}

We’llalso need to prepare some test data. This can easily be done by using curl tosave the raw response from bitly to a JSON file and loading that again for eachtest case as described in this previous post.

我們也需要準(zhǔn)備一些測試數(shù)據(jù)绅作。這很容易:如上一篇博文所說,我們可以用curl來保存從bitly到JSON文件的原始應(yīng)答蛾派,然后在每個測試用例中加載出來俄认。

Putting it all Together

動手組裝

Finallywe’ll need to write some tests that verifies the ILBitly code. As an exampleone of the tests for the shortening service is shown below:

最后,我們寫些測試來驗(yàn)證ILBitly代碼洪乍。例如眯杏,下文是一個驗(yàn)證縮短URL服務(wù)的測試:

-(void)testShorten {

// Prepare the canned test result

[ILCannedURLProtocolsetCannedResponseData:[self cannedDataWithName:@"shorten"]];

[ILCannedURLProtocol setCannedHeaders:

[NSDictionarydictionaryWithObject:@"application/json; charset=utf-8"

forKey:@"Content-Type"]];

// Prepare the mock

bitlyMock = [OCMockObjectpartialMockForObject:bitly];

NSURL *trigger = [NSURLURLWithString:@"http://"];

[[[bitlyMock expect] andReturn:[NSURLRequestrequestWithURL:trigger]]

requestForURLString:[OCMArgcheckWithBlock:^(id url) {

return [urlisEqualToString:EXPECTED_REQUEST];

}]];

// Execute the code under test

[bitlyshorten:@"http://www.infinite-loop.dk/blog/" result:^(NSString *result){

STAssertEqualObjects(result,@"http://j.mp/qA7S4Q", @"Unexpected short url");

done = YES;

} error:^(NSError *err) {

STFail(@"Shorten failed with error:%@", [err localizedDescription]);

done = YES;

}];

// Verify the result

STAssertTrue([self waitForCompletion:5.0],@"Timeout");

[bitlyMock verify];

}

Inthe first part the static test data is loaded into the test protocol.

在第一部分中,靜態(tài)測試數(shù)據(jù)被加載到測試協(xié)議中壳澳。

Nexta partial mock object is created for the bitly object. Its primary role is tointercept the internal call to requestForURLString: and setup an expectationthat it’s actually being called. Once that call is made it will verify that theexpected url is requested and finally return an instance of NSURLRequest. Thatinstance just contains enough of the basic url scheme in order to trigger theload of our custom protocol.

之后我們?yōu)閎itly對象創(chuàng)建了部分mock對象岂贩。它的主要功能是攔截對requestForURLString的內(nèi)部調(diào)用,并創(chuàng)建一個我們期望調(diào)用的URL巷波。調(diào)用時萎津,測試會驗(yàn)證是否向我們期望的URL發(fā)出了請求卸伞,并最終返回一個NSURLRequest實(shí)例。為觸發(fā)加載我們自定義Protocol锉屈,該實(shí)例只包含了基本的URL Scheme荤傲。

Thecode under test can now be executed as shown in the third part. Since theblocks may be called at any time after invoking the shorten:result:error:method done is set so we know when it has been called.

被測試的代碼可如第三部分所示被執(zhí)行。由于invoke shorten:result:error后block隨時可能被回調(diào)颈渊,我們設(shè)置了done遂黍,這樣一來被調(diào)用時我們就能知道了。

Thefinal part of the code then waits for up to 5 seconds for done to be set asdetailed in a previous post. Finally verify is called on the mock object toensure that the expected messages were received.

如上一篇博文所述俊嗽,最后的一段代碼將會給done信號最多5秒的等待時間雾家。最后,確認(rèn)mock對象被調(diào)回绍豁,從而確認(rèn)已經(jīng)收到了所期望的信息芯咧。

Ifwe instead want to test for proper handling of errors we’ll just have toreplace the first part of the test method so it sets up error data and changethe tests accordingly:

如果我們轉(zhuǎn)而想測試系統(tǒng)對錯誤的處理,我們只需替換掉測試方法的第一部分妹田,改為錯誤數(shù)據(jù)唬党,同時相應(yīng)地對測試做如下改動:

[ILCannedURLProtocol setCannedError:

[NSError errorWithDomain:NSURLErrorDomain

code:kCFURLErrorTimedOut

userInfo:nil]];

Conclusion

結(jié)論

AsI’ve shown above it’s possible to use NSURLProtocol for injecting predictabletest data into unit and mock tests that would otherwise have been subject toexternal factors. It’s also possible to extend these tests even further. Forinstance you could use this method for implementing various simulations of badnetwork conditions such as high latencies and low bandwidth. The possibilitiesare endless and I just hope that this post at least have provided someinspiration.

綜上所述鹃共,我們可以利用NSURLProtocol鬼佣,將可預(yù)測的測試數(shù)據(jù)注入單元測試和mock測試中,以減少外部因素的影響霜浴。我們甚至可以擴(kuò)展這些測試晶衷。舉例來說,你可以用這個方法模擬糟糕的網(wǎng)絡(luò)環(huán)境阴孟,如長延遲和窄帶寬晌纫。可能性是無窮的永丝,我僅希望可用此文拋磚引玉锹漱。

TheILBitly wrapper as well as the accompanying test classes used in this post areavailable on GitHub along with a sample iPhone app that demonstrates some of thefunctionality.

本文中所使用的ILBitly包及測試類都可在GitHub上找到,同時我還放了一個iPhone

APP樣例慕嚷,用以演示某些功能哥牍。

Update:The ILCannedURLProtocol class is now included in the ILTesting repository onGithub.

更新:ILCannedURLProtocol類也已放到Github的ILTesting庫中。

Commentsand suggestions are welcome as always.

歡迎各類評論與建議喝检。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗅辣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子挠说,更是在濱河造成了極大的恐慌澡谭,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件损俭,死亡現(xiàn)場離奇詭異蛙奖,居然都是意外死亡潘酗,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門雁仲,熙熙樓的掌柜王于貴愁眉苦臉地迎上來崎脉,“玉大人,你說我怎么就攤上這事伯顶∏糇疲” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵祭衩,是天一觀的道長灶体。 經(jīng)常有香客問我,道長掐暮,這世上最難降的妖魔是什么蝎抽? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮路克,結(jié)果婚禮上樟结,老公的妹妹穿的比我還像新娘。我一直安慰自己精算,他們只是感情好瓢宦,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著灰羽,像睡著了一般驮履。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上廉嚼,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天玫镐,我揣著相機(jī)與錄音,去河邊找鬼怠噪。 笑死恐似,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傍念。 我是一名探鬼主播矫夷,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼捂寿!你這毒婦竟也來了口四?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤秦陋,失蹤者是張志新(化名)和其女友劉穎蔓彩,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡赤嚼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年旷赖,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片更卒。...
    茶點(diǎn)故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡等孵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蹂空,到底是詐尸還是另有隱情俯萌,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布上枕,位于F島的核電站咐熙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏辨萍。R本人自食惡果不足惜棋恼,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望锈玉。 院中可真熱鬧爪飘,春花似錦、人聲如沸拉背。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽去团。三九已至抡诞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間土陪,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工肴熏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鬼雀,地道東北人。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓蛙吏,卻偏偏與公主長得像源哩,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子鸦做,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評論 2 345

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

  • PLEASE READ THE FOLLOWING APPLE DEVELOPER PROGRAM LICENSE...
    念念不忘的閱讀 13,433評論 5 6
  • 15 和諧的獲得 托斯卡納和羅馬励烦,16世紀(jì)初期 16 世紀(jì)絕對是個偉大的時代,這期間有達(dá).芬奇和米開朗其羅泼诱,...
    季玫閱讀 264評論 0 0
  • 第二天早上我們玩早間游戲玩了以后我們就開始上課坛掠,到了下午亮仔給我們講了他的童年,到了晚上我們開了派隊(duì)老師拿了許多的...
    王智海軒閱讀 102評論 1 0
  • 文/文子先生 自由的白襯衫 忘不了中分頭的傻樣 瞇起眼的小黑鏡 任由拖鞋嘀嗒響 流氓的口哨 伴著嘻哈小曲兒嘰喳唱 ...
    文子先生閱讀 350評論 0 3
  • 再次迎來了9月,看著早上食堂中穿梭的大一新生屉栓,穿著當(dāng)年我們很嫌棄的軍訓(xùn)服舷蒲,再看看學(xué)弟學(xué)妹妹的臉龐,依稀看到了當(dāng)年青...
    蔓薇芷閱讀 350評論 3 1