如何在單元測(cè)試中處理異步回調(diào)函數(shù)

歡迎回來腹泌,這一節(jié)凉袱,我們基于之前實(shí)現(xiàn)的MockURLSessionMockURLSessionDataTask來測(cè)試WeatherDataManager中和網(wǎng)絡(luò)通信相關(guān)的功能。

該怎么做呢专甩?

為了回答這個(gè)問題,我們首先應(yīng)該考慮的問題是:究竟想要測(cè)試什么钉稍?例如,在上一節(jié)中贡未,我們的目的是:測(cè)試resume()方法被調(diào)用。那么俊卤,現(xiàn)在呢?我們可以從一個(gè)最簡(jiǎn)單的場(chǎng)景開始:確保服務(wù)器的返回結(jié)果不為nil消恍。

為了測(cè)試這個(gè)結(jié)果,最簡(jiǎn)單的辦法當(dāng)然就是實(shí)際向DarkSky發(fā)送一個(gè)請(qǐng)求哺哼,然后測(cè)試返回值叼风,為此,我們就“跟著感覺”寫下了下面這個(gè)測(cè)試用例:

func test_weatherDataAt_gets_data() {
    var data: WeatherData? = nil

    WeatherDataManager.shared.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: { (response, error) in
        data = response
    })

    XCTAssertNotNil(data)
}

執(zhí)行一下測(cè)試就會(huì)發(fā)現(xiàn)无宿,Xcode會(huì)直接告訴我們測(cè)試失敗了。這是因?yàn)槟跫Γ琗code的測(cè)試用例是單線程執(zhí)行的,它不會(huì)等待weatherDataAt的回調(diào)函數(shù)執(zhí)行完彬碱。因此,在執(zhí)行XCTAssertNotNil時(shí)巷疼,網(wǎng)絡(luò)請(qǐng)求還沒有完成,此時(shí)data的值還是nil嚼沿。于是,測(cè)試就失敗了瓷患。

該怎么辦呢?我們有兩種方法測(cè)試異步執(zhí)行的回調(diào)函數(shù)擅编。

使用Xcode expectation

第一種,是使用在Xcode 6時(shí)引入的一個(gè)功能爱态,叫做Expectation。簡(jiǎn)單來說肢藐,就是允許我們?cè)谝粋€(gè)時(shí)間范圍里吱韭,給Xcode設(shè)置一個(gè)“期望”,如果期望滿足了就表示測(cè)試成功理盆,如果超時(shí)了,就表示測(cè)試失敗猿规。

直接來看代碼。首先姨俩,我們用expectation方法蘸拔,給“期望”添加一個(gè)描述:

func test_weatherDataAt_gets_data() {
    let expect = expectation(
        description: "Loading data from \(API.authenticatedURL)")
    /// ...
}

其次,在我們之前編寫的代碼里调窍,在條件滿足的地方,調(diào)用fulfill()方法通知Xcode:

func test_weatherDataAt_gets_data() {
    let expect = expectation(description: "Loading data from \(API.authenticatedURL)")

    var data: WeatherData?
    WeatherDataManager.shared.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: { (response, error) in
        data = response
        expect.fulfill() // - Notify Xcode here
    })

    /// ...
}

最后邓萨,給“期望值”設(shè)置一個(gè)超時(shí)時(shí)間,并進(jìn)行測(cè)試:

func test_weatherDataAt_gets_data() {
    /// ...

    waitForExpectations(timeout: 5, handler: nil)
    XCTAssertNotNil(data)
}

這樣缔恳,只要在5秒之內(nèi)得到了返回值,測(cè)試就會(huì)成功歉甚,否則,就會(huì)失敗铃芦。重新執(zhí)行一下測(cè)試,如果一切順利刃滓,我們就可以看到測(cè)試通過的結(jié)果了仁烹。

但是咧虎,通過Xcode expectation也只能部分解決我們的問題,對(duì)于測(cè)試異步執(zhí)行的代碼砰诵,這種方式仍有一些問題:

  • 首先,測(cè)試結(jié)果仍舊取決于網(wǎng)絡(luò)狀況茁彭,因此我們很難保證多次測(cè)試結(jié)果的一致性;
  • 其次理肺,當(dāng)我們要測(cè)試一個(gè)REST服務(wù)的時(shí)候,如果每個(gè)URL的測(cè)試都基于實(shí)際網(wǎng)絡(luò)訪問和超時(shí)的機(jī)制妹萨,將會(huì)顯著增加測(cè)試執(zhí)行的時(shí)間年枕;

為此乎完,我們需要需要第二種方法:把從網(wǎng)絡(luò)獲取到數(shù)據(jù)的部分mock出來,并且,讓異步執(zhí)行的代碼同步執(zhí)行桥状,這樣才可以精確管理測(cè)試用例的執(zhí)行過程。

借助于上一節(jié)實(shí)現(xiàn)的MockURLSessionMockURLSessionDataTask典格,我們可以很容易完成這兩個(gè)工作。

通過mock串行化異步執(zhí)行的代碼

為了控制從服務(wù)器得到的是正常的響應(yīng)或是發(fā)生了錯(cuò)誤耍缴,我們給MockURLSession添加三個(gè)屬性:

class MockURLSession: URLSessionProtocol {
    var responseData: Data?
    var responseHeader: HTTPURLResponse?
    var responseError: Error?

    /// ...
}

這樣,我們就可以直接通過設(shè)置這三個(gè)屬性的值來模擬從服務(wù)器得到的返回結(jié)果了防嗡。接下來,我們還要調(diào)整MockURLSession.dataTask的實(shí)現(xiàn):

class MockURLSession: URLSessionProtocol {
    /// ...

    func dataTask(
        with request: URLRequest,
        completionHandler: @escaping DataTaskHandler)
        -> URLSessionDataTaskProtocol {
        completionHandler(responseData, responseHeader, responseError)
        return sessionDataTask
    }
}

由于不用通過網(wǎng)絡(luò)請(qǐng)求數(shù)據(jù)了蚁趁,在這個(gè)“仿制”的版本里,我們直接調(diào)用dataTask的回調(diào)函數(shù)就好了他嫡。這樣,就把一個(gè)異步回調(diào)方法钢属,在測(cè)試的過程中變成了一個(gè)同步方法徘熔。

重新審視weatherDataAt的實(shí)現(xiàn)

接下來淆党,在開始編寫測(cè)試用例之前,我們?cè)賮砜匆谎?code>weatherDataAt的代碼染乌。它是一個(gè)testable的方法么?

func weatherDataAt(
    latitude: Double,
    longitude: Double,
    completion: @escaping CompletionHandler) {
    /// ...

    self.urlSession.dataTask(
        with: request,
        completionHandler: {
        (data, response, error) in
        DispatchQueue.main.async {
            self.didFinishGettingWeatherData(
                data: data,
                response: response,
                error: error,
                completion: completion)
        }
    }).resume()

在當(dāng)初我們給dataTask傳遞closure參數(shù)的時(shí)候說過荷憋,這部分代碼很可能會(huì)和更新UI相關(guān),因此勒庄,直接把它放在了主線程隊(duì)列中執(zhí)行。這看似沒什么不合理锅铅,但當(dāng)我們引入了單元測(cè)試之后酪呻,就有了新的發(fā)現(xiàn):

  • 首先盐须,有了剛才的經(jīng)歷我們就會(huì)知道漆腌,這樣并不利于測(cè)試阶冈。盡管我們讓dataTask的回調(diào)函數(shù)本身變成了同步執(zhí)行,但在這個(gè)closure內(nèi)的代碼卻是異步執(zhí)行的塑径,因此我們?nèi)耘f無法可靠地獲取調(diào)用weatherDataAt之后的結(jié)果;
  • 其次统舀,在面向?qū)ο蟮脑O(shè)計(jì)里,你也可能聽說過這樣的說法:盡可能把在設(shè)計(jì)上的決策推后到你真正需要它們的時(shí)候誉简。因?yàn)橐坏Q定了碉就,它就會(huì)成為制約你后面所有設(shè)計(jì)的一個(gè)限制闷串。那么,回過頭來想這個(gè)問題:我們一定會(huì)在這更新UI么烹吵?當(dāng)代碼日益復(fù)雜之后,我們?nèi)绾斡浀靡呀?jīng)把closure參數(shù)放在了主線程里呢肋拔?似乎我們也都沒有特別有信心的答案;

因此只损,基于上面兩點(diǎn)考慮一姿,我們不應(yīng)該限制dataTask clousure的執(zhí)行環(huán)境:

self.urlSession.dataTask(
    with: request,
    completionHandler: {
    (data, response, error) in
    self.didFinishGettingWeatherData(
        data: data,
        response: response,
        error: error,
        completion: completion)
    }).resume()

沒錯(cuò)跃惫,直接調(diào)用它就好了,如果要更新UI爆存,我們應(yīng)該明確在closure里指出讓代碼在主線程中執(zhí)行。這樣weatherDataAt的實(shí)現(xiàn)先较,就可以在測(cè)試環(huán)境里,用同步的方式執(zhí)行了闲勺。

設(shè)計(jì)測(cè)試用例

一切都準(zhǔn)備就緒之后曾棕,我們來設(shè)計(jì)weatherDataAt方法的測(cè)試用例菜循。

測(cè)試可以正確的處理請(qǐng)求錯(cuò)誤

第一個(gè)要測(cè)試的內(nèi)容,是可以處理錯(cuò)誤的請(qǐng)求。根據(jù)我們自己的實(shí)現(xiàn)衙耕,這種情況下,應(yīng)該可以得到DataManagerError.failedRequest橙喘。在WeatherDataManagerTest里,添加下面的代碼:

 func test_weatherDataAt_handle_invalid_request() {
    let session = MockURLSession()
    session.responseError = NSError(
        domain: "Invalid Request",
        code: 100,
        userInfo: nil)

    let manager = WeatherDataManager(
        baseURL: URL(string: "https://darksky.net")!,
        urlSession: session)

    var error: DataManagerError? = nil
    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.failedRequest)
}

測(cè)試可以正確處理服務(wù)器返回的狀態(tài)碼

第二個(gè)要測(cè)試的內(nèi)容厅瞎,是可以檢測(cè)到服務(wù)器返回的非200 HTTP狀態(tài)碼,對(duì)這種情況和簸,我們也會(huì)得到``DataManagerError.failedRequest`:

func test_weatherDataAt_handle_statuscode_not_equal_to_200() {
    let url = URL(string: "https://darksky.net")!
    let session = MockURLSession()
    session.responseHeader = HTTPURLResponse(
        url: url, statusCode: 400,
        httpVersion: nil,
        headerFields: nil)

    let data = "{}".data(using: .utf8)!
    session.responseData = data

    var error: DataManagerError? = nil
    let manager = WeatherDataManager(
        baseURL: url, urlSession: session)

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.failedRequest)
}

測(cè)試服務(wù)器返回的內(nèi)容不正確

下一個(gè)要測(cè)試的內(nèi)容,是服務(wù)器返回HTTP 200的時(shí)候比搭,附帶的數(shù)據(jù)不完整的情況冠跷。這次蜜托,我們故意創(chuàng)造一個(gè)非法的JSON字符串形式:{。并期望得到DataManagerError.failedRequest

func test_weatherDataAt_handle_invalid_response() {
    let url = URL(string: "https://darksky.net")!
    let session = MockURLSession()
    session.responseHeader = HTTPURLResponse(
        url: url,
        statusCode: 200,
        httpVersion: nil,
        headerFields: nil)

    /// Make a invalid JSON response here
    let data = "{".data(using: .utf8)!
    session.responseData = data

    var error: DataManagerError? = nil
    let manager = WeatherDataManager(
        baseURL: url, urlSession: session)

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.invalidResponse)
}

測(cè)試可以正確解碼服務(wù)器返回值

最后橄务,應(yīng)該測(cè)試合法的情況了。我們測(cè)試服務(wù)器的返回值可以自動(dòng)解碼成model對(duì)象:

func test_weatherDataAt_handle_response_decode() {
    let url = URL(string: "https://darksky.net")!
    let session = MockURLSession()
    session.responseHeader = HTTPURLResponse(
        url: url,
        statusCode: 200,
        httpVersion: nil,
        headerFields: nil)

    let data = """
    {
        "longitude" : 100,
        "latitude" : 52,
        "currently" : {
            "temperature" : 23,
            "humidity" : 0.91,
            "icon" : "snow",
            "time" : 1507180335,
            "summary" : "Light Snow"
        }
    }
    """.data(using: .utf8)!
    session.responseData = data

    var decoded: WeatherData? = nil
    let manager = WeatherDataManager(
        baseURL: url, urlSession: session)

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
            (d, _) in
            decoded = d
    })

    let expected = WeatherData(
        latitude: 52,
        longitude: 100,
        currently: WeatherData.CurrentWeather(
            time: Date(timeIntervalSince1970: 1507180335),
            summary: "Light Snow",
            icon: "snow",
            temperature: 23,
            humidity: 0.91))

    XCTAssertEqual(decoded, expected)
}

雖然看著有點(diǎn)長(zhǎng)蜂挪,但是邏輯很簡(jiǎn)單,我們只是比較手工創(chuàng)建的WeatherData對(duì)象和解碼出來的結(jié)果是否相同罷了嗓化。但為了上面的代碼可以通過測(cè)試,我們還得做一些修改刺覆。

首先,為了讓WeatherData對(duì)象支持比較谦屑,我們得讓它遵從protocol Equatable。在WeatherData.swift中氢橙,添加下面的代碼:

extension WeatherData.CurrentWeather: Equatable {
    static func ==(
        lhs: WeatherData.CurrentWeather,
        rhs: WeatherData.CurrentWeather) -> Bool {
        return lhs.time == rhs.time &&
            lhs.summary == rhs.summary &&
            lhs.icon == rhs.icon &&
            lhs.temperature == rhs.temperature &&
            lhs.humidity == rhs.humidity
    }
}

extension WeatherData: Equatable {
    static func ==(lhs: WeatherData,
        rhs: WeatherData) -> Bool {
        return lhs.latitude == rhs.latitude &&
            lhs.longitude == rhs.longitude &&
            lhs.currently == rhs.currently
    }
}

都是直接比較屬性相等的代碼酝枢,很簡(jiǎn)單悍手。

其次喉磁,由于DarkSky返回的是UNIX時(shí)間戳,我們要在解碼的時(shí)候,設(shè)置一下Date對(duì)象的解碼方式涝焙。把didFinishGettingWeatherData中解碼的部分改成下面這樣:

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
let weatherData = try decoder.decode(
    WeatherData.self, from: data)

完成后,test_weatherData_handle_response_decode()就應(yīng)該可以通過測(cè)試了仑撞。

Refactor the test case

最后赤兴,我們整理一下所有的測(cè)試用例隧哮,把其中公共的部分定義成屬性,把這些屬性的設(shè)置沮翔,統(tǒng)一放到setUp方法里,Xcode會(huì)在執(zhí)行每一個(gè)測(cè)試方法前執(zhí)行這些代碼

這也是implicitly unwrapped optional的一個(gè)典型的應(yīng)用場(chǎng)景采蚀。

class WeatherDataManagerTest: XCTestCase {
    let url = URL(string: "https://darksky.net")!
    var session: MockURLSession!
    var manager: WeatherDataManager!

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
        self.session = MockURLSession()
        self.manager = WeatherDataManager(baseURL: url, urlSession: session)
    }

    /// ...
}

下面,是基于這些調(diào)整之后的測(cè)試用例完整代碼:

func test_weatherDataAt_starts_the_session() {
    let dataTask = MockURLSessionDataTask()
    session.sessionDataTask = dataTask

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: { _, _ in  })

    XCTAssert(session.sessionDataTask.isResumeCalled)
}

func test_weatherDataAt_handle_invalid_request() {
    session.responseError = NSError(
        domain: "Invalid Request", code: 100, userInfo: nil)
    var error: DataManagerError? = nil

    manager.weatherDataAt(latitude: 52, longitude: 100, completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.failedRequest)
}

func test_weatherDataAt_handle_statuscode_not_equal_to_200() {
    session.responseHeader = HTTPURLResponse(
        url: url, statusCode: 400, httpVersion: nil, headerFields: nil)

    let data = "{}".data(using: .utf8)!
    session.responseData = data

    var error: DataManagerError? = nil

    manager.weatherDataAt(latitude: 52, longitude: 100, completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.failedRequest)
}

func test_weatherDataAt_handle_invalid_response() {
    session.responseHeader = HTTPURLResponse(
        url: url,
        statusCode: 200,
        httpVersion: nil,
        headerFields: nil)

    let data = "{".data(using: .utf8)!
    session.responseData = data

    var error: DataManagerError? = nil

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
        (_, e) in
        error = e
    })

    XCTAssertEqual(error, DataManagerError.invalidResponse)
}

func test_weatherDataAt_handle_response_decode() {
    session.responseHeader = HTTPURLResponse(
        url: url,
        statusCode: 200,
        httpVersion: nil,
        headerFields: nil)

    let data = """
    {
        "longitude" : 100,
        "latitude" : 52,
        "currently" : {
            "temperature" : 23,
            "humidity" : 0.91,
            "icon" : "snow",
            "time" : 1507180335,
            "summary" : "Light Snow"
        }
    }
    """.data(using: .utf8)!
    session.responseData = data

    var decoded: WeatherData? = nil

    manager.weatherDataAt(
        latitude: 52,
        longitude: 100,
        completion: {
            (d, _) in
            decoded = d
    })

    let expected = WeatherData(
        latitude: 52,
        longitude: 100,
        currently: WeatherData.CurrentWeather(
            time: Date(timeIntervalSince1970: 1507180335),
            summary: "Light Snow",
            icon: "snow",
            temperature: 23,
            humidity: 0.91))

    XCTAssertEqual(decoded, expected)
}

至此榆鼠,我們就可以確定model的解碼以及manager都可以正常工作了。稍后妆够,當(dāng)我們編寫界面的時(shí)候,還會(huì)繼續(xù)討論UI測(cè)試的方法神妹。通過這個(gè)過程颓哮,我們可以看到鸵荠,單元測(cè)試,不僅可以有助于更早發(fā)現(xiàn)錯(cuò)誤腰鬼,也可以在某種程度上改進(jìn)代碼的質(zhì)量。現(xiàn)在熄赡,把測(cè)試的話題先放放姜挺。在下一節(jié)彼硫,我們來定義Sky的view controllers凌箕,并把它們和models關(guān)聯(lián)起來。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末牵舱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子缺虐,更是在濱河造成了極大的恐慌,老刑警劉巖高氮,帶你破解...
    沈念sama閱讀 218,682評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異剪芍,居然都是意外死亡塞淹,警方通過查閱死者的電腦和手機(jī)罪裹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,277評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來状共,“玉大人,你說我怎么就攤上這事口芍」坎” “怎么了鬓椭?”我有些...
    開封第一講書人閱讀 165,083評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)小染。 經(jīng)常有香客問我翘瓮,道長(zhǎng)裤翩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,763評(píng)論 1 295
  • 正文 為了忘掉前任踊赠,我火速辦了婚禮,結(jié)果婚禮上筐带,老公的妹妹穿的比我還像新娘今穿。我一直安慰自己伦籍,他們只是感情好腮出,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,785評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著胚嘲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪洛二。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,624評(píng)論 1 305
  • 那天灭红,我揣著相機(jī)與錄音口注,去河邊找鬼。 笑死寝志,一個(gè)胖子當(dāng)著我的面吹牛娇斑,可吹牛的內(nèi)容都是我干的材部。 我是一名探鬼主播,決...
    沈念sama閱讀 40,358評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼乐导,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼苦丁!你這毒婦竟也來了物臂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,261評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤棵磷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后仪媒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體沉桌,經(jīng)...
    沈念sama閱讀 45,722評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡留凭,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了偎巢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,030評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡艘狭,死狀恐怖翠订,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情遵倦,我是刑警寧澤,帶...
    沈念sama閱讀 35,737評(píng)論 5 346
  • 正文 年R本政府宣布梧躺,位于F島的核電站,受9級(jí)特大地震影響掠哥,放射性物質(zhì)發(fā)生泄漏巩踏。R本人自食惡果不足惜续搀,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,360評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望禁舷。 院中可真熱鬧彪杉,春花似錦牵咙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,941評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽另凌。三九已至,卻和暖如春途茫,著一層夾襖步出監(jiān)牢的瞬間碟嘴,已是汗流浹背囊卜。 一陣腳步聲響...
    開封第一講書人閱讀 33,057評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留栅组,地道東北人雀瓢。 一個(gè)月前我還...
    沈念sama閱讀 48,237評(píng)論 3 371
  • 正文 我出身青樓玉掸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親司浪。 傳聞我的和親對(duì)象是個(gè)殘疾皇子泊业,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,976評(píng)論 2 355

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

  • 原文地址:https://www.raywenderlich.com/150073/ios-unit-testin...
    默默熊閱讀 4,906評(píng)論 1 67
  • 前言 單元測(cè)試和UI測(cè)試大致步驟網(wǎng)上很多文章都有,如果會(huì)的可以忽略饮睬,關(guān)鍵是錯(cuò)誤總結(jié),網(wǎng)上很少有文章提及到篮奄,感興趣的...
    _YGL_閱讀 5,175評(píng)論 20 23
  • 單元測(cè)試不是一個(gè)小工程,需要多用些時(shí)間才能做好窟却,不要希望通過這個(gè)文章就能掌握單元測(cè)試昼丑,這只是一個(gè)入門夸赫,需要自己動(dòng)手...
    勇不言棄92閱讀 7,819評(píng)論 9 60
  • 一、簡(jiǎn)介 單元測(cè)試(Unit Testing) 是一種軟件測(cè)試方法憔足,主要用于確定各個(gè)獨(dú)立的軟件模塊是否正確胁附。在這個(gè)...
    陽光下的灰塵閱讀 1,224評(píng)論 0 2
  • 前言: 對(duì)于單元測(cè)試來說滓彰,我想大部分同行,在項(xiàng)目中揭绑,很少會(huì)用到,也有一大部分郎哭,知道單元測(cè)試這個(gè)東西,但是確切的說沒...
    Andy_FML閱讀 1,043評(píng)論 0 1