Achieving Testability in Presentation Layer

Achieving Testability in Presentation Layer

8889da6a67db3667b0694d993c9a962c?s=128

Yosuke Ishikawa

January 15, 2019
Tweet

Transcript

  1. දࣔϩδοΫʹ͓͚Δ
 ςελϏϦςΟͷ֫ಘྫ JTILBXB

  2. w גࣜձࣾ9$50 w 4XJGU,PUMJO(P+BWB4DSJQU w "1*,JU%*,JU%BUB4PVSDF,JU w 4XJGU࣮ફೖ໳J041SPHSBNNJOH

  3. ςελϏϦςΟͷ࿩

  4. w ςετΛॻ͖ͮΒ͍Օॴ͕͋Δ w ίετ͕ݟ߹͍ͬͯͳ͍ؾ͕͢Δ w ϝϯς͕ͭΒ͘ͳ͖ͬͯͨ w ։ൃ଎౓͕མ͖ͪͯͨ

  5. զʑ͕औΓ૊ΜͩྫΛ঺հ͠·͢ ʢϕετϓϥΫςΟεͱ͸ݶΒͳ͍ʣ

  6. ྫ୊

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

    w ϦετΛදࣔ͢Δ w ⭐ͷλοϓ࣌ w Ϙλϯͷঢ়ଶΛ൓స w αʔόʔʹ൓సޙͷঢ়ଶΛૹ৴
  9. "1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ

  10. "1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ ্͔ΒԼ·ͰҰؾʹ ςετ͢Δͷ͸େม

  11. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ λΠϜϥΠϯͷ੍ޚ͕೉͍͠ ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍

  12. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ λΠϜϥΠϯͷ੍ޚ͕೉͍͠ ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍ w 6*ͷཁૉΛ௨ͯ͡ঢ়ଶΛݕূ͢Δඞཁ͕͋Δ w ଴͕ͪଟ͘ͳΔͨΊ൓෮࣮ߦʹ޲͔ͳ͍

  13. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ λΠϜϥΠϯͷ੍ޚ͕೉͍͠ ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍ w 6*΍֎෦γεςϜͷΠϕϯτ͸׬શʹ͸੍ޚͰ͖ͳ͍ w ࿈ଧͳͲͷࠐΈೖͬͨঢ়گΛ࠶ݱͰ͖ͳ͍

  14. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ λΠϜϥΠϯͷ੍ޚ͕೉͍͠ ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍ w ֎෦γεςϜͷԠ౴Λ੾Γସ͑Δඞཁ͕͋Δ w ؀ڥʹΑͬͯෆ҆ఆͳ݁ՌʹͳΔ

  15. ͭͣͭղফ͢Δ

  16. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ

  17. ݕূ͠΍͍͢ϞσϧͰදݱ͢Δ

  18. None
  19. 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) }
  20. 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) } σʔλΛอ࣋͢ΔϓϩύςΟ
  21. 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) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ
  22. 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) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ ηϧΛදݱ͢Δܕ
  23. 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) }
  24. 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) }
  25. XCTAssertEqual(state.cells, [.empty(isLoading: true)])

  26. XCTAssertEqual(state.cells, [.empty(isLoading: false)])

  27. XCTAssertEqual(state.cells, [ .repository(repository1), .repository(repository2), .repository(repository3), ... ])

  28. ঢ়ଶΛݕূ͠΍͘͢ͳͬͨ

  29. ঢ়ଶΛݕূ͠΍͘͢ͳͬͨ ʢ࣮ࡍͷදࣔ͸ݕূ͠ͳ͍͔Βʣ

  30. "1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ 6*ςετ

  31. "1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ ಺෦ঢ়ଶͷϢχοτςετ

  32. 6*ςετ͸͍Βͳ͍ʁ

  33. ͦΜͳ͜ͱ͸ͳ͍

  34. w 6*ςετͰͳ͚Ε͹ݕূͰ͖ͳ͍͜ͱ΋͋Δ w ঢ়گʹԠͯ͡࢖͍෼͚Δͱྑ͍

  35. λΠϜϥΠϯͷ੍ޚ͕೉͍͠

  36. Ծ૝্࣌ؒͰΠϕϯτΛѻ͏

  37. 3Y4XJGU

  38. 3FBDUJWF4XJGU

  39. let starredIndex = scheduler.createHotObservable([ next(1, 3), next(2, 4), next(3, 5),

    ])
  40. Ծ૝࣌ؒ͸୯ͳΔ਺஋ͳͷͰ ࣗ༝ʹ੍ޚͰ͖Δ

  41. Ծ૝ͳͷͰඵ΋Ұॠ

  42. ݁Ռ΋Ծ૝࣌ؒͰݕূͰ͖Δ

  43. 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), ... ]), ])
  44. ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍

  45. ґଘΛϓϩτίϧʹͯ͠ελϒԽ͢Δ

  46. protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>

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

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

    } final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...} w ςετͰ͸ͪ͜ΒΛ࢖͏ w ςετίʔυͰࢦఆͨ͠ϨεϙϯεΛฦ͢ w ࣮ࡍͷαʔόʔʹ͸ΞΫηε͠ͳ͍
  49. ελϒ͚ͩͳΒ࣮૷͸؆୯ ʢϥΠϒϥϦΛ࢖ͬͯ΋ྑ͍͚Ͳʣ

  50. 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) } } }
  51. 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) } } } ελϒԽ͢ΔϦΫΤετͱ ϨεϙϯεͷϖΞΛొ࿥͓ͯ͘͠
  52. 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) } } } ϦΫΤετ͕དྷͨ࣌ʹελϒͱ
 Ϛον͢Δ΋ͷ͕͋Ε͹ฦ͢
  53. let apiClient = TestAPIClient() apiClient.stub( request: ListRepositoriesRequest(), response: Single .just(ListRepositoriesResponse(repositories:

    [])) .delay(5, scheduler: scheduler)) let viewController = RepositoriesViewController(apiClient: apiClient)
  54. ςετέʔε͝ͱʹ ೚ҙͷϨεϙϯε͕ฦͤΔ

  55. ৼΓฦΓ

  56. 6*ςετ͸࣮૷΋࣮ߦ΋ߴίετ
 ˠঢ়ଶΛݕূ͠΍͍͢ϞσϧͰදݱ͢Δ λΠϜϥΠϯͷ੍ޚ͕೉͍͠
 ˠԾ૝্࣌ؒͰΠϕϯτΛѻ͏ ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍
 ˠґଘΛϓϩτίϧʹͯ͠ελϒԽ͢Δ

  57. %FNP

  58. ςετ͠΍͍͢౔ඨͰઓ͓͏"

  59. IUUQTHJUIVCDPNJTILBXBJPT@NWWN@UFTU@FYBNQMF