Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Achieving Testability in Presentation Layer

Achieving Testability in Presentation Layer

Avatar for Yosuke Ishikawa

Yosuke Ishikawa

January 15, 2019
Tweet

More Decks by Yosuke Ishikawa

Other Decks in Technology

Transcript

  1. w ը໘දࣔ࣌ w ΠϯδέʔλʔΛදࣔ w ಡΈࠐΈΛ։࢝ w ಡΈࠐΈ׬ྃ࣌ w ΠϯδέʔλʔΛඇදࣔʹ

    w ϦετΛදࣔ͢Δ w ⭐ͷλοϓ࣌ w Ϙλϯͷঢ়ଶΛ൓స w αʔόʔʹ൓సޙͷঢ়ଶΛૹ৴
  2. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) }
  3. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) } σʔλΛอ࣋͢ΔϓϩύςΟ
  4. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ
  5. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ ηϧΛදݱ͢Δܕ
  6. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) }
  7. struct State { var repositories = [] as [Repository] var

    isLoading = false var cells: [Cell] { if repositories.isEmpty { return [.empty(isLoading: isLoading)] } else { return repositories.map { .repository($0) } } } } enum Cell: Equatable { case empty(isLoading: Bool) case repository(Repository) }
  8. XCTAssertEqual(cells.events, [ next(0, [ ... .repository(repository4), .repository(repository5), .repository(repository6), ... ]),

    next(1, [ ... .repository(repository4Starred), .repository(repository5), .repository(repository6), ... ]), next(2, [ ... .repository(repository4Starred), .repository(repository5Starred), .repository(repository6), ... ]), next(3, [ ... .repository(repository4Starred), .repository(repository5Starred), .repository(repository6Starred), ... ]), ])
  9. protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>

    } final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...}
  10. protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>

    } final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...} w ΞϓϦͰ͸ͪ͜ΒΛ࢖͏ w ࣮ࡍͷαʔόʔʹΞΫηε͢Δ
  11. protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>

    } final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...} w ςετͰ͸ͪ͜ΒΛ࢖͏ w ςετίʔυͰࢦఆͨ͠ϨεϙϯεΛฦ͢ w ࣮ࡍͷαʔόʔʹ͸ΞΫηε͠ͳ͍
  12. final class TestAPIClient: APIClient { private var stubs = []

    as [(request: Any, response: Any)] func stub<Request: APIRequest>(request: Request, response: Single<Request.Response>) { stubs.append((request: request, response: response)) } func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response> { if let index = stubs.firstIndex(where: { ($0.request as? Request) == request }) { let stub = stubs.remove(at: index) return stub.response as! Single<Request.Response> } else { return Single.error(RxError.unknown) } } }
  13. final class TestAPIClient: APIClient { private var stubs = []

    as [(request: Any, response: Any)] func stub<Request: APIRequest>(request: Request, response: Single<Request.Response>) { stubs.append((request: request, response: response)) } func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response> { if let index = stubs.firstIndex(where: { ($0.request as? Request) == request }) { let stub = stubs.remove(at: index) return stub.response as! Single<Request.Response> } else { return Single.error(RxError.unknown) } } } ελϒԽ͢ΔϦΫΤετͱ ϨεϙϯεͷϖΞΛొ࿥͓ͯ͘͠
  14. final class TestAPIClient: APIClient { private var stubs = []

    as [(request: Any, response: Any)] func stub<Request: APIRequest>(request: Request, response: Single<Request.Response>) { stubs.append((request: request, response: response)) } func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response> { if let index = stubs.firstIndex(where: { ($0.request as? Request) == request }) { let stub = stubs.remove(at: index) return stub.response as! Single<Request.Response> } else { return Single.error(RxError.unknown) } } } ϦΫΤετ͕དྷͨ࣌ʹελϒͱ
 Ϛον͢Δ΋ͷ͕͋Ε͹ฦ͢
  15. let apiClient = TestAPIClient() apiClient.stub( request: ListRepositoriesRequest(), response: Single .just(ListRepositoriesResponse(repositories:

    [])) .delay(5, scheduler: scheduler)) let viewController = RepositoriesViewController(apiClient: apiClient)