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

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. දࣔϩδοΫʹ͓͚Δ

    ςελϏϦςΟͷ֫ಘྫ
    JTILBXB

    View Slide

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

    View Slide

  3. ςελϏϦςΟͷ࿩

    View Slide

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

    View Slide

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

    View Slide

  6. ྫ୊

    View Slide

  7. View Slide

  8. w ը໘දࣔ࣌
    w ΠϯδέʔλʔΛදࣔ
    w ಡΈࠐΈΛ։࢝
    w ಡΈࠐΈ׬ྃ࣌
    w ΠϯδέʔλʔΛඇදࣔʹ
    w ϦετΛදࣔ͢Δ
    w ⭐ͷλοϓ࣌
    w Ϙλϯͷঢ়ଶΛ൓స
    w αʔόʔʹ൓సޙͷঢ়ଶΛૹ৴

    View Slide

  9. "1*$MJFOU
    3FQPTJUPSJFT7JFX$POUSPMMFS
    6*$PMMFDUJPO7JFX
    3FQPTJUPSZ$FMM &NQUZ$FMM
    3FQPTJUPSZ

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  15. ͭͣͭղফ͢Δ

    View Slide

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

    View Slide

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

    View Slide

  18. View Slide

  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)
    }

    View Slide

  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)
    }
    σʔλΛอ࣋͢ΔϓϩύςΟ

    View Slide

  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)
    }
    ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ

    View Slide

  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)
    }
    ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ
    ηϧΛදݱ͢Δܕ

    View Slide

  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)
    }

    View Slide

  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)
    }

    View Slide

  25. XCTAssertEqual(state.cells, [.empty(isLoading: true)])

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  33. ͦΜͳ͜ͱ͸ͳ͍

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  37. 3Y4XJGU

    View Slide

  38. 3FBDUJWF4XJGU

    View Slide

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

    View Slide

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

    View Slide

  41. Ծ૝ͳͷͰඵ΋Ұॠ

    View Slide

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

    View Slide

  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),
    ...
    ]),
    ])

    View Slide

  44. ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  48. protocol APIClient {
    func sendRequest(_ request: Request) -> Single
    }
    final class AppAPIClient: APIClient {...}
    final class TestAPIClient: APIClient {...}
    w ςετͰ͸ͪ͜ΒΛ࢖͏
    w ςετίʔυͰࢦఆͨ͠ϨεϙϯεΛฦ͢
    w ࣮ࡍͷαʔόʔʹ͸ΞΫηε͠ͳ͍

    View Slide

  49. ελϒ͚ͩͳΒ࣮૷͸؆୯
    ʢϥΠϒϥϦΛ࢖ͬͯ΋ྑ͍͚Ͳʣ

    View Slide

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

    View Slide

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

    View Slide

  52. final class TestAPIClient: APIClient {
    private var stubs = [] as [(request: Any, response: Any)]
    func stub(request: Request, response: Single) {
    stubs.append((request: request, response: response))
    }
    func sendRequest(_ request: Request) -> Single {
    if let index = stubs.firstIndex(where: { ($0.request as? Request) == request }) {
    let stub = stubs.remove(at: index)
    return stub.response as! Single
    } else {
    return Single.error(RxError.unknown)
    }
    }
    }
    ϦΫΤετ͕དྷͨ࣌ʹελϒͱ

    Ϛον͢Δ΋ͷ͕͋Ε͹ฦ͢

    View Slide

  53. let apiClient = TestAPIClient()
    apiClient.stub(
    request: ListRepositoriesRequest(),
    response: Single
    .just(ListRepositoriesResponse(repositories: []))
    .delay(5, scheduler: scheduler))
    let viewController = RepositoriesViewController(apiClient: apiClient)

    View Slide

  54. ςετέʔε͝ͱʹ
    ೚ҙͷϨεϙϯε͕ฦͤΔ

    View Slide

  55. ৼΓฦΓ

    View Slide

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

    ˠঢ়ଶΛݕূ͠΍͍͢ϞσϧͰදݱ͢Δ
    λΠϜϥΠϯͷ੍ޚ͕೉͍͠

    ˠԾ૝্࣌ؒͰΠϕϯτΛѻ͏
    ςετର৅ͷঢ়گΛ༻ҙͮ͠Β͍

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

    View Slide

  57. %FNP

    View Slide

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

    View Slide

  59. View Slide