Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Achieving Testability in Presentation Layer
Search
Yosuke Ishikawa
January 15, 2019
Technology
4k
4
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Achieving Testability in Presentation Layer
Sample code:
https://github.com/ishkawa/ios_mvvm_test_example
Yosuke Ishikawa
January 15, 2019
More Decks by Yosuke Ishikawa
See All by Yosuke Ishikawa
効率的な開発手段として VRTを活用する
ishkawa
1
250
アプリを起動せずにアプリを開発して品質と生産性を上げる
ishkawa
0
4.6k
Introducing Wire: Dependency Injection by Code Generator
ishkawa
12
1.4k
Declarative UICollectionView
ishkawa
28
8.6k
Nuxt.jsが掲げる"Universal Vue.js Applications"とは何者か
ishkawa
10
2.9k
Static Dependency Injection by Code Generation
ishkawa
15
6.8k
実践クライアントサイドSwift
ishkawa
23
4.4k
JSON-RPC on APIKit
ishkawa
5
69k
RxSwiftは開発をどう変えたか?
ishkawa
12
4.2k
Other Decks in Technology
See All in Technology
WEBフロントエンド研修【MIXI 26新卒技術研修】
mixi_engineers
PRO
2
800
基調講演:人とAIをつなぐIoTの今と未来 ー 「フィジカル」と「デジタル」が出会うその先へ【SORACOM Discovery 2026】
soracom
PRO
0
360
ウォーターフォール開発案件のPMとしてAI活用を模索している話
hatahata021
2
230
Amazon Bedrock Managed Knowledge BaseDive Deep
ren8k
0
280
20260608_Codexの可能性_ノンプログラマー向け_大城追記
doradora09
PRO
0
270
toio・myCobotでフィジカルAIっぽいことを行うための検討(とりあえず調査) / フィジカルAI LT(IoTLTによる開催)
you
PRO
0
140
QAと開発の両側から進める AI活用 -QAプロセスAI支援ツールキットと Inner Loop / Outer Loopの取り組み-
legalontechnologies
PRO
2
380
「待ち時間」の消滅と「自我消耗」の加速:生成AI時代のエンジニアを救うメンタル・リソース管理
poropinai1966
0
360
AIがコードを書く時代、人間は何を保証するのか———馬場さんと考える、開発者に求められる新しい責任と価値 - TECH PLAY
netmarkjp
0
330
運用を犠牲にせずコストを制御し事業成長を支える B2B SaaS ID管理基盤におけるS3 Tableのログストレージ活用
kaminashi
1
120
クラウドを使う側から、作る側へ / 大吉祥寺.pm 2026前夜祭
fujiwara3
8
1.9k
『モンスターストライク』 の運営に伴走する! データ民主化への 解析グループの3つのアプローチ
mixi_engineers
PRO
0
180
Featured
See All Featured
Let's Do A Bunch of Simple Stuff to Make Websites Faster
chriscoyier
508
140k
Money Talks: Using Revenue to Get Sh*t Done
nikkihalliwell
0
440
Ecommerce SEO: The Keys for Success Now & Beyond - #SERPConf2024
aleyda
1
2.1k
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
1.7k
AI Search: Where Are We & What Can We Do About It?
aleyda
0
7.7k
Leadership Guide Workshop - DevTernity 2021
reverentgeek
1
330
DBのスキルで生き残る技術 - AI時代におけるテーブル設計の勘所
soudai
PRO
67
56k
Stewardship and Sustainability of Urban and Community Forests
pwiseman
0
400
Visual Storytelling: How to be a Superhuman Communicator
reverentgeek
2
600
Tell your own story through comics
letsgokoyo
1
1k
Measuring & Analyzing Core Web Vitals
bluesmoon
9
940
How to train your dragon (web standard)
notwaldorf
97
6.7k
Transcript
දࣔϩδοΫʹ͓͚Δ ςελϏϦςΟͷ֫ಘྫ JTILBXB
w גࣜձࣾ9$50 w 4XJGU,PUMJO(P+BWB4DSJQU w "1*,JU%*,JU%BUB4PVSDF,JU w 4XJGU࣮ફೖJ041SPHSBNNJOH
ςελϏϦςΟͷ
w ςετΛॻ͖ͮΒ͍Օॴ͕͋Δ w ίετ͕ݟ߹͍ͬͯͳ͍ؾ͕͢Δ w ϝϯς͕ͭΒ͘ͳ͖ͬͯͨ w ։ൃ͕མ͖ͪͯͨ
զʑ͕औΓΜͩྫΛհ͠·͢ ʢϕετϓϥΫςΟεͱݶΒͳ͍ʣ
ྫ
None
w ը໘දࣔ࣌ w ΠϯδέʔλʔΛදࣔ w ಡΈࠐΈΛ։࢝ w ಡΈࠐΈྃ࣌ w ΠϯδέʔλʔΛඇදࣔʹ
w ϦετΛදࣔ͢Δ w ⭐ͷλοϓ࣌ w Ϙλϯͷঢ়ଶΛస w αʔόʔʹసޙͷঢ়ଶΛૹ৴
"1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ
"1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ ্͔ΒԼ·ͰҰؾʹ ςετ͢Δͷେม
6*ςετ࣮࣮ߦߴίετ λΠϜϥΠϯͷ੍ޚ͕͍͠ ςετରͷঢ়گΛ༻ҙͮ͠Β͍
6*ςετ࣮࣮ߦߴίετ λΠϜϥΠϯͷ੍ޚ͕͍͠ ςετରͷঢ়گΛ༻ҙͮ͠Β͍ w 6*ͷཁૉΛ௨ͯ͡ঢ়ଶΛݕূ͢Δඞཁ͕͋Δ w ͕ͪଟ͘ͳΔͨΊ෮࣮ߦʹ͔ͳ͍
6*ςετ࣮࣮ߦߴίετ λΠϜϥΠϯͷ੍ޚ͕͍͠ ςετରͷঢ়گΛ༻ҙͮ͠Β͍ w 6*֎෦γεςϜͷΠϕϯτશʹ੍ޚͰ͖ͳ͍ w ࿈ଧͳͲͷࠐΈೖͬͨঢ়گΛ࠶ݱͰ͖ͳ͍
6*ςετ࣮࣮ߦߴίετ λΠϜϥΠϯͷ੍ޚ͕͍͠ ςετରͷঢ়گΛ༻ҙͮ͠Β͍ w ֎෦γεςϜͷԠΛΓସ͑Δඞཁ͕͋Δ w ڥʹΑͬͯෆ҆ఆͳ݁ՌʹͳΔ
ͭͣͭղফ͢Δ
6*ςετ࣮࣮ߦߴίετ
ݕূ͍͢͠ϞσϧͰදݱ͢Δ
None
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) }
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) } σʔλΛอ࣋͢ΔϓϩύςΟ
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) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ
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) } ը໘ͷঢ়ଶΛදݱ͢ΔϓϩύςΟ ηϧΛදݱ͢Δܕ
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) }
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) }
XCTAssertEqual(state.cells, [.empty(isLoading: true)])
XCTAssertEqual(state.cells, [.empty(isLoading: false)])
XCTAssertEqual(state.cells, [ .repository(repository1), .repository(repository2), .repository(repository3), ... ])
ঢ়ଶΛݕূ͘͢͠ͳͬͨ
ঢ়ଶΛݕূ͘͢͠ͳͬͨ ʢ࣮ࡍͷදࣔݕূ͠ͳ͍͔Βʣ
"1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ 6*ςετ
"1*$MJFOU 3FQPTJUPSJFT7JFX$POUSPMMFS 6*$PMMFDUJPO7JFX 3FQPTJUPSZ$FMM &NQUZ$FMM 3FQPTJUPSZ ෦ঢ়ଶͷϢχοτςετ
6*ςετ͍Βͳ͍ʁ
ͦΜͳ͜ͱͳ͍
w 6*ςετͰͳ͚ΕݕূͰ͖ͳ͍͜ͱ͋Δ w ঢ়گʹԠ͍͚ͯ͡Δͱྑ͍
λΠϜϥΠϯͷ੍ޚ͕͍͠
Ծ্࣌ؒͰΠϕϯτΛѻ͏
3Y4XJGU
3FBDUJWF4XJGU
let starredIndex = scheduler.createHotObservable([ next(1, 3), next(2, 4), next(3, 5),
])
Ծ࣌ؒ୯ͳΔͳͷͰ ࣗ༝ʹ੍ޚͰ͖Δ
ԾͳͷͰඵҰॠ
݁ՌԾ࣌ؒͰݕূͰ͖Δ
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), ... ]), ])
ςετରͷঢ়گΛ༻ҙͮ͠Β͍
ґଘΛϓϩτίϧʹͯ͠ελϒԽ͢Δ
protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>
} final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...}
protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>
} final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...} w ΞϓϦͰͪ͜ΒΛ͏ w ࣮ࡍͷαʔόʔʹΞΫηε͢Δ
protocol APIClient { func sendRequest<Request: APIRequest>(_ request: Request) -> Single<Request.Response>
} final class AppAPIClient: APIClient {...} final class TestAPIClient: APIClient {...} w ςετͰͪ͜ΒΛ͏ w ςετίʔυͰࢦఆͨ͠ϨεϙϯεΛฦ͢ w ࣮ࡍͷαʔόʔʹΞΫηε͠ͳ͍
ελϒ͚ͩͳΒ࣮؆୯ ʢϥΠϒϥϦΛͬͯྑ͍͚Ͳʣ
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) } } }
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) } } } ελϒԽ͢ΔϦΫΤετͱ ϨεϙϯεͷϖΞΛొ͓ͯ͘͠
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) } } } ϦΫΤετ͕དྷͨ࣌ʹελϒͱ Ϛον͢Δͷ͕͋Εฦ͢
let apiClient = TestAPIClient() apiClient.stub( request: ListRepositoriesRequest(), response: Single .just(ListRepositoriesResponse(repositories:
[])) .delay(5, scheduler: scheduler)) let viewController = RepositoriesViewController(apiClient: apiClient)
ςετέʔε͝ͱʹ ҙͷϨεϙϯε͕ฦͤΔ
ৼΓฦΓ
6*ςετ࣮࣮ߦߴίετ ˠঢ়ଶΛݕূ͍͢͠ϞσϧͰදݱ͢Δ λΠϜϥΠϯͷ੍ޚ͕͍͠ ˠԾ্࣌ؒͰΠϕϯτΛѻ͏ ςετରͷঢ়گΛ༻ҙͮ͠Β͍ ˠґଘΛϓϩτίϧʹͯ͠ελϒԽ͢Δ
%FNP
ςετ͍͢͠ඨͰઓ͓͏"
IUUQTHJUIVCDPNJTILBXBJPT@NWWN@UFTU@FYBNQMF