Swift TDD & async URLSession - how to test?

末鹿安然 提交于 2019-12-22 11:14:02

问题


I try to get familiar with TDD. How can I test URLSession calls which are asynchronous ? Which XCAssert better to use and where, in which stage ?

My first thought was to create a function, that has URLSession inside it, and inside this function set bool flag to true and then test it in XCAssertTrue. Or another thought was to return fake data synchronously after calling function which contains USLSession code.


回答1:


Nice question. I think it can be decomposed in two parts, the first is about how to test asynchronous code using XCTest, the second is about the strategies that you suggest.

The problem with asynchronous code is that it runs on a different thread as the test, which results in the test moving on after the async call, and finishing before that has completed.

The XCTest framework provides a waitForExpectationsWithTimeout:handler function that is built just for this use case. It allows you to wait for the async call to have finished and only then check its result.

This is how you could use it:

import XCTest
@testable import MyApp

class CallbackTest: XCTestCase {

  func testAsyncCalback() {
    let service = SomeService()

    // 1. Define an expectation
    let expectation = expectationWithDescription("SomeService does stuff and runs the callback closure")

    // 2. Exercise the asynchronous code
    service.doSomethingAsync { success in
      XCTAssertTrue(success)

      // Don't forget to fulfill the expectation in the async callback
      expectation.fulfill()
    }

    // 3. Wait for the expectation to be fulfilled
    waitForExpectationsWithTimeout(1) { error in
      if let error = error {
        XCTFail("waitForExpectationsWithTimeout errored: \(error)")
      }
    }
  }
}

You can read more about the approach in this blog post. Full disclosure, I wrote it.

Now, regarding your suggestions:

My first thought was to create a function, that has URLSession inside it, and inside this function set bool flag to true and then test it in XCAssertTrue.

Good idea. That's a great way to test if an asynchronous callback is actually called. The only thing to keep in mind is that the test needs to wait for the async call to run, or it will always fail.

Using the technique above you could write:

let service = SomeService()
var called = false

let expectation = expectationWithDescription(
    "The callback passed to SomeService is actually called"
)

service.doSomethingAsync { _ in
  called = true
  expectation.fulfill()
}

waitForExpectationsWithTimeout(1) { error in
  if let error = error {
    XCTFail("waitForExpectationsWithTimeout errored: \(error)")
  }

  XCTAssertTrue(called)
}

Or another thought was to return fake data synchronously after calling function which contains URLSession code.

That's another great idea. Decomposing software in components that do only one thing, test them all thoroughly in isolation, and finally test that they all work together with a few integration tests that stress the happy and most common failure paths.

Since you are mentioning URLSession I'd like to leave a warning note regarding unit testing code that touches the network. Having real network calls in your unit tests is generally a bad idea. In fact, it binds the tests to the fact that the network will be available and return the expected result, and also makes them slower.

At a unit level we want our test to be isolated, deterministic, and fast (more on this here). Hitting the network doesn't align with these goals.

A good approach to avoid hitting the network is to stub it, either by wrapping URLSession into a protocol and then using a fake conforming to it in your tests, or by using a library like OHHTTPStubs. This post goes more into details, again full disclosure, I wrote this one too.

Hope this helps. Unit testing asynchronous code is not trivial, you should be patient with it and keep asking question if something doesn't seem right :)




回答2:


Similar to testing the GUI, I wouldn't advise testing the backend directly from within your client app code. The problem is mainly inconsistent state from the network - how do you maintain stable tests if, for example, your database can change? If your code doesn't change, but your test results do, that's not good.

Instead, you're right about testing fake data. I would recommend putting your network service behind some kind of interface, and then depending on the testing strategy you want to go with, somehow provide test doubles that "implement" that interface throughout your tests.

Rather than test your application's network interactions directly, testing your domain layer with pre-defined domain model objects (as if they have already been transformed from network JSON/XML) should give you more precise control over your tests, which is good.

Another reason why I suggest this is to protect your time and sanity. Maintaining predictable, precise tests can be hard enough, particularly when you refactor your application code. Adding a potentially-shifting externality on to that is a risk.




回答3:


To add to points from @drhr, consider a Facade pattern to abstract away the network specifics. In the constructor pass in an specific implementation that wraps the network functionality, i.e in live, code that used Networking, in test, code that stubs the networking.

The aim for this type of Unit Test is to test how your code responds to the different responses from the network code, you are not aiming to test the actual network elements here (that can be done in integration/system testing if required).




回答4:


I would use mocks to fake URLSession, real unit test and speedup development http://ocmock.org/ is a great tool to do that. If you still want to test URLSession you can use the code bellow which use XCT expectations and threads. Different from others answers, this test expects the function under test to run URLSession inside it with a completition handler which will set the boolean value (called myFlag). The function itself is not async at all, just part of it called by URLSession. We will have to use expectations a little different:

func testAFunc(){
    let expect = expectation(description: "...")
    DispatchQueue.global(qos: .userInitiated).async { // 1
        let myObject = MyObject()
        myObject.myFuncUnderTestWhichCallURLSession() // the function which calls URLSession
        while(true){ 

            if( myObject.myFlag == nil ){
                XCTAssertEqual(true, myObject.myFlag)
                expect.fulfill()
                break;
            }
        }
    }

    waitForExpectations(timeout: 5000) { error in
        // ...
    }

Why threads? expectations expects an fulfill to be called. In this example we are not calling an async code direcly, so I cannot call fulfill inside it. Used an while loop to call it when the flag is changed (after an URLSession response) inside a thread so it can reach the waitForExpectations.



来源:https://stackoverflow.com/questions/40330692/swift-tdd-async-urlsession-how-to-test

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!