Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥

Achieving Testability in Presentation Layer

Achieving Testability in Presentation Layer

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)