我是ios Unit Testing的新手,这是我第一次真正做到这一点。
我的应用程序正在使用一个正在进行网络调用的框架,
我试图通过调用其中一个函数来测试框架的业务逻辑,但函数只是通过实际调用,并且永远不会进入回调。
这是我试图检查的框架内的实际功能>
func initSDK () {
let launchParams: [String:String] = [
kAWSDKUrl: WhiteLabeler.localizedStringForKey(key: "baseSDKUrl", comment: "-", defaultValue: "https://isr-lap-tst2.americanwell.com:8443/"),
kAWSDKKey: WhiteLabeler.localizedStringForKey(key: "SDKserviceKeyForIos", comment: "-", defaultValue: "TriageApp"),
kAWSDKBundleID: Bundle.main.bundleIdentifier!
]
AWSDKService.initialize(withLaunchParams: launchParams) {[weak self] (success, error) in
if success {
self?.myPresenter?.onSDKInitlized()
self?.didSdkInitilized = true
} else {
self?.myPresenter?.onError(errorText: error?.localizedDescription ?? "")
}
}
}
这是我的测试用例:
import XCTest
@testable import TriageFramework
class Virtual_First_Tests: XCTestCase {
override func setUp() {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample1() {
let homeInteractor: HomeInteractor = HomeInteractor();
var didInitlized = homeInteractor.didSdkInitilized
homeInteractor.initSDK()
sleep(2)
didInitlized = homeInteractor.didSdkInitilized
XCTAssertTrue(didInitlized)
}
但它始终失败,因为它永远不会成功或失败的回调。我在这做错了什么?
您不应该在单元测试中进行网络呼叫。单元测试用于测试隔离的单个类。因此,您应该将网络调用或inject your dependencies存入您正在测试的类中(在您的情况下为HomeInteractor
)。此外,您的单元测试需要快速运行,在单元测试中进行网络调用将做相反的事情。
Here有一篇帖子可以帮助你创建好的单元测试。
关于要测试的代码,有几点需要指出:
initialize
方法的闭包回调中。这意味着测试需要考虑他们必须等待调用闭包的事实。作为Enrique Bermúdez notes,您应该始终避免在单元或集成测试中进行网络调用。命中服务器会使测试运行速度变慢,测试应该是fast,这样您就可以快速获得反馈。最重要的是,您希望测试是可预测的,但进行网络呼叫可能会因任何原因而失败,例如超时,服务器关闭或连接中断。
我知道将自己与服务器的依赖关系和AWS SDK的第三方代码隔离开的最好方法是在其前面放置协议,并将其替换为test double。
protocol AWSSDKWrapper {
static func initialize() // this should actually match the signature of the
// initialize method on AWSSDKService, but I don't
// know how it looks like
}
extension AWSSDKService: AWSSDKWrapper { }
然后,您可以在类型的init
中注入AWS的依赖项。
class HomeInteractor {
private let awsSDKWrapper: AWSSDKWrapper
init(awsSDKWrapper: AWSSDKWrapper = AWSSDKService.self) {
self.awsSDKWrapper = awsSDKWrapper
}
func initSDK () {
let launchParams: [String:String] = // ...
awsSDKWrapper.initialize(withLaunchParams: launchParams) {[weak self] (success, error) in
if success {
self?.myPresenter?.onSDKInitlized()
self?.didSdkInitilized = true
} else {
self?.myPresenter?.onError(errorText: error?.localizedDescription ?? "")
}
}
}
}
现在,您在第三方和它所做的网络请求之间都有一层抽象。我们可以构建自己的test double来控制测试中的行为。
struct AWSSDKStub: AWSSDKWrapper {
let result: Result<Bool, NSError> // I'm assuming the type of error the AWSSDK
// callback returns is NSError.
// I'm also assuming you have Swift 5 Result
// available, if you don't check this Gist for
// a lightweight drop-in replacement
init(succeeding: Bool) {
self.result = .success(succeeding)
}
init(error: NSError) {
self.result = .failure(error)
}
func initialize(/* again not sure how the arguments look like */) {
switch result {
case .success(let succeeded): callback(succeeded)
case .failure(let error): error
}
}
}
根据您要测试的行为,您可能想要也可能不想添加探测值来捕获传递给初始化的launchParams。
现在让我们使用这个对象来控制测试中的行为。
func testAWSSDKInitializeSuccess() {
let homeInteractor = HomeInteractor(awsSDWWrapper: AWSSDKStub(succeeding: true))
// Because the test is asynchronous we need to setup the expectation _before_
// calling its method.
//
// More here: https://www.mokacoding.com/blog/xctest-closure-based-expectation/
let predicate = NSPredicate(block: { any, _ in
return (any as? HomeIterator)?.didSdkInitilized == true
})
_ = self.expectation(for: predicate, evaluatedWith: homeIterator, handler: .none)
homeIntera.initSDK()
waitForExpectations(timeout: 1, handler: .none)
}
这种方法的优点在于我们还可以针对AWS SDK初始化返回false或错误的情况编写测试。如果我们不断触及实际的AWS端点,我们就无法做到这一点,因为我们无法控制它的响应方式。
在仅公开我们的应用程序感兴趣的API的协议中包装第三方依赖项是有价值的,不仅因为它允许我们提供测试双精度并更好地测试代码如何与依赖项交互,还因为它允许我们不必更改我们的代码如果依赖项改变,只有包装器。另见dependency inversion principle。
调用包装协议Wrapper
是一种气味。理想情况下,您使用的名称可以捕获您正在使用的第三方功能的子集。
我们写的测试检查didSdkInitilized
是否设置为true
。我想问你的问题是“这是HomeInteractor
的实际行为,还是只是一个实现细节?”如果不了解更多关于HomeIterator应该做什么的话,很难回答,但我的猜测是didSdkInitilized
只是一个实现细节,该方法的真实行为是在其演示者上调用onSDKInitlized()
或onError(errorText:)
。
编写专注于行为的测试而不是实现细节总是更好。当您想要重构代码时,专注于实现细节的测试会妨碍您,即更改其实现而不改变其行为方式。从长远来看,专注于行为的测试可以帮助您。
在您的情况下,测试HomeInteractor
在第三方SDK成功初始化时是否调用演示者的可能方法是使用测试双重作为演示者,然后检查其方法是否已被调用。更多关于这种方法here。
希望这可以帮助。如果你想在Swift中更多地谈论测试,请在@mokagio的Twitter上发帖。