Sourceryを使ったSwiftのモック

まえがき



iOSアプリケーションの開発中、開発者はコードの単体テストのタスクに直面する場合があります。 これはまさに私が直面した問題です。







挑戦する



認証アプリケーションがあるとしましょう。 認証については、認証サービスであるAuthenticationServiceを担当します。 たとえば、2つのメソッドがあり、両方ともユーザーを認証しますが、1つは同期で、もう1つは非同期です。







protocol AuthenticationService { typealias Login = String typealias Password = String typealias isSucces = Bool ///    /// /// - Parameters: /// - login:   /// - password:  /// - Returns:   func authenticate(with login: Login, and password: Password) -> isSucces ///     /// /// - Parameters: /// - login:   /// - password:  /// - authenticationHandler: Callback(completionHandler)  func asyncAuthenticate(with login: Login, and password: Password, authenticationHandler: @escaping (isSucces) -> Void) }
      
      





このサービスを使用するviewControllerがあります。







 class ViewController: UIViewController { var authenticationService: AuthenticationService! var login = "Login" var password = "Password" ///  ,     var aunthenticationHandler: ((Bool) -> Void) = { (isAuthenticated) in print("\n  :") isAuthenticated ? print(" ") : print("  ") } override func viewDidLoad() { super.viewDidLoad() authenticationService = AuthenticationServiceImplementation() // -   ,   , ..    viewController performAuthentication() performAsyncAuthentication() } func performAuthentication() { let isAuthenticated = authenticationService.authenticate(with: login, and: password) print("  :") isAuthenticated ? print(" ") : print("  ") } func performAsyncAuthentication() { authenticationService.asyncAuthenticate(with: login, and: password, and: aunthenticationHandler) } }
      
      





viewControllerをテストする必要があります。







解決策



なぜなら viewControllerのクラスを除く他のオブジェクトにテストが依存しないようにするには、すべての依存関係をウェットにします。 これを行うために、認証サービススタブを作成します。 次のようになります。







 class MockAuthenticationService: AuthenticationService { var emulatedResult: Bool? // ,      var receivedLogin: AuthenticationService.Login? //      var receivedPassword: AuthenticationService.Password? //      var receivedAuthenticationHandler: ((AuthenticationService.isSucces) -> Void)? // ,             func authenticate(with login: AuthenticationService.Login, and password: AuthenticationService.Password) -> AuthenticationService.isSucces { receivedLogin = login receivedPassword = password return emulatedResult ?? false } func asyncAuthenticate(with login: AuthenticationService.Login, and password: AuthenticationService.Password, and authenticationHandler: @escaping (AuthenticationService.isSucces) -> Void) { receivedLogin = login receivedPassword = password receivedAuthenticationHandler = authenticationHandler } }
      
      





依存関係ごとに非常に多くのコードを手動で書くのはあまり快適ではありません(依存関係のプロトコルが変更されたときに書き換えるのは特に良いことです)。 この問題の解決策を探し始めました。 私は、モッキートの類似体を見つけることを考えていました(アンドロイド開発に携わっている同僚によってスパイされました)。 検索中に、swiftが読み取り専用リフレクションをサポートしていることがわかりました(ランタイムでは、オブジェクトに関する情報のみを検索でき、オブジェクトの動作を変更できますが、できません)。 したがって、そのようなライブラリはありません。 必死、トースターで質問した。 ソリューションは、 Vyacheslav BeltyukovMan with a Bear(ManWithBear)を促しました







Sourceryでmokiを生成します。 Sourceryはテンプレートを使用してコードを生成します。 いくつかの標準的なものがありますが、AutoMockableは私たちの目的に適しています。







ビジネスに取り掛かろう:







1)ポッド「Sourcery」をプロジェクトに追加します。

2)プロジェクト用にRunScriptを構成します。







 $PODS_ROOT/Sourcery/bin/sourcery --sources . --templates ./Pods/Sourcery/Templates/AutoMockable.stencil --output ./SwiftMocking
      
      





どこで:







「$ PODS_ROOT / Sourcery / bin / sourcery」は、Sourcery実行可能ファイルへのパスです。

「-ソース」。 -コード生成のために分析する対象の表示(ドットはプロジェクトの現在のフォルダーを示します。つまり、プロジェクトの各ファイルに対してmokiを生成する必要があるかどうかを確認します)。

「--templates ./Pods/Sourcery/Templates/AutoMockable.stencil」-コード生成テンプレートへのパス。

"--output ./SwiftMocking"-コード生成の結果が保存される場所(プロジェクトはSwiftMockingと呼ばれます)。







3)AutoMockable.swiftファイルをプロジェクトに追加します。







 ///    ,     protocol AutoMockable {}
      
      





4)モックするプロトコルは、AutoMockableから継承する必要があります。 この場合、AuthenticationServiceを継承します。







 protocol AuthenticationService: AutoMockable {
      
      





5)プロジェクトをビルドします。 --ouputパラメーターとして指定したパスであるフォルダーにAutoMockable.generated.swiftファイルが生成され、その中に生成されたmokiが置かれます。 後続のすべてのmokiはこのファイルに追加されます。







6)このファイルをプロジェクトに追加します。 これでスタブを使用できます。







認証サービスプロトコル用に生成されたものを見てみましょう。







 class AuthenticationServiceMock: AuthenticationService { //MARK: - authenticate var authenticateCalled = false var authenticateReceivedArguments: (login: Login, password: Password)? var authenticateReturnValue: isSucces! func authenticate(with login: Login, and password: Password) -> isSucces { authenticateCalled = true authenticateReceivedArguments = (login: login, password: password) return authenticateReturnValue } //MARK: - asyncAuthenticate var asyncAuthenticateCalled = false var asyncAuthenticateReceivedArguments: (login: Login, password: Password, authenticationHandler: (isSucces) -> Void)? func asyncAuthenticate(with login: Login, and password: Password, and authenticationHandler: @escaping (isSucces) -> Void) { asyncAuthenticateCalled = true asyncAuthenticateReceivedArguments = (login: login, password: password, authenticationHandler: authenticationHandler) } }
      
      





素晴らしい。 これで、テストでスタブを使用できます。







 import XCTest @testable import SwiftMocking class SwiftMockingTests: XCTestCase { var viewController: ViewController! var authenticationService: AuthenticationServiceMock! override func setUp() { super.setUp() authenticationService = AuthenticationServiceMock() viewController = ViewController() viewController.authenticationService = authenticationService viewController.login = "Test login" viewController.password = "Test password" } func testPerformAuthentication() { // given authenticationService.authenticateReturnValue = true // when viewController.performAuthentication() // then XCTAssert(authenticationService.authenticateReceivedArguments?.login == viewController.login, "      ") XCTAssert(authenticationService.authenticateReceivedArguments?.password == viewController.password, "      ") XCTAssert(authenticationService.authenticateCalled, "    ") } func testPerformAsyncAuthentication() { // given var isAuthenticated = false viewController.aunthenticationHandler = { isAuthenticated = $0 } // when viewController.performAsyncAuthentication() authenticationService.asyncAuthenticateReceivedArguments?.authenticationHandler(true) // then XCTAssert(authenticationService.asyncAuthenticateCalled, "     ") XCTAssert(authenticationService.asyncAuthenticateReceivedArguments?.login == viewController.login, "       ") XCTAssert(authenticationService.asyncAuthenticateReceivedArguments?.password == viewController.password, "       ") XCTAssert(isAuthenticated, "    ") } }
      
      





おわりに



Sourceryはスタブを書き込み、時間を節約します。 このユーティリティには他の用途があります:プロジェクト内の構造のEquatable拡張を生成(これらの構造のオブジェクトを比較できるようにするため)。







便利なリンク



プロジェクト

githubのソース

ドキュメントのソース








All Articles