2016-10-25 3 views
1

Im Versuch, Dependency Injection mit Protokollen in Swift zu erreichen :)Mocking eine Klasse mit Rx-Erweiterungen

im Test Mein System ruft .rx.signIn() auf die Abhängigkeit bedeutet, dass es Reactive wo Basis erweitert: ConcreteObject .

Ist es möglich, ein Protokoll einzurichten, das bereit ist, .rx-Aufrufe zu empfangen?

Jedes kleine Beispiel oder Alternative würde sehr geschätzt werden.

Antwort

1

Ich hatte das gleiche Problem während des Upgrades von RxSwift2 zu RxSwift3. In RxSwift2 verwendete ich einfach ein Protokoll für die rx-Methoden in meinem Produktionscode (zB rx_JSON()) und injizierte ein einfaches Mock-Objekt, das diesem Protokoll in meinen Unit-Tests entsprach.

Nach einem kurzen Abstecher in Generika, assoziiert Art und Typ Löschung ich mit dem folgenden Setup angesiedelt, die eng meine RxSwift2-Setup passt:

Zum Beispiel URLSession als die konkrete Umsetzung meiner HTTP-Anrufe zu verstecken, ich habe ein Protokoll Remoteclient:

protocol RemoteClient { 
    func json(url: URL) -> Observable<Any> 
} 

Dann in meiner Produktion Code verweisen ich nur dieses Protokoll:

class SomeService { 
    var remoteClient: RemoteClient 

    init(remoteClient: RemoteClient) { 
     self.remoteClient = remoteClient 
    } 

    func getSomeData(_ id: String) -> Observable<JSONDictionary> { 
     let urlString = "..." 

     return remoteClient 
      .json(url: URL(string: urlString)!) 
      .map { data in 
       guard let show = data as? JSONDictionary else { 
        throw ParsingError.json 
       } 

       return show 
      } 
    } 
} 

in meinem Unit-Tests habe ich Mock-Objekte, die auf dem Remoteclient-Protokoll einzuhalten:

class RemoteClientMock: RemoteClient { 

    var output: Any? 
    var verifyInput: ((URL) -> Void)? 

    func json(url: URL) -> Observable<Any> { 
     verifyInput?(url) 

     return Observable.create { observer in 
      if let data = self.output { 
       observer.on(.next(data)) 
      } 

      observer.on(.completed) 

      return Disposables.create() 
     } 
    } 
} 

Und sie in dem getesteten System injizieren:

class SomeServiceTest: TestCase { 

    let disposeBag = DisposeBag() 


    func testGetSomeData() { 
     // setup 
     let expectation = self.expectation(description: "request succeeded") 

     let remoteClientMock = RemoteClientMock() 
     remoteClientMock.verifyInput = { url in 
      XCTAssertEqual(URL(string: "some url"), url) 
     } 
     remoteClientMock.output = ["id": 4711] 

     let service = SomeService(remoteClient: remoteClientMock) 

     // exercise 
     let observable = service.getSomeData("4711") 

     // verify 
     observable 
      .subscribe(onNext: { data in 
       XCTAssertEqual(4711, data["id"] as? Int) 
       expectation.fulfill() 
      }) 
      .addDisposableTo(disposeBag) 

     waitForExpectations(timeout: 1, handler: nil) 
    } 
} 

So einfach ich den rx-Namensraum hinter meinem Protokoll verbergen. Ich mache das jetzt mit einem Adapter:

let remoteClient = URLSessionRemoteClientAdapter(urlSession: URLSession.shared) 
let service = SomeService(remoteClient: remoteClient) 

struct URLSessionRemoteClientAdapter: RemoteClient { 
    let urlSession: URLSession 

    init(urlSession: URLSession) { 
     self.urlSession = urlSession 
    } 

    func json(url: URL) -> Observable<Any> { 
     return urlSession.rx.json(url: url) 
    } 
}