Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
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
270
アプリを起動せずにアプリを開発して品質と生産性を上げる
ishkawa
0
4.7k
Introducing Wire: Dependency Injection by Code Generator
ishkawa
12
1.5k
Declarative UICollectionView
ishkawa
28
8.7k
Nuxt.jsが掲げる"Universal Vue.js Applications"とは何者か
ishkawa
10
2.9k
Static Dependency Injection by Code Generation
ishkawa
15
6.9k
実践クライアントサイドSwift
ishkawa
23
4.5k
JSON-RPC on APIKit
ishkawa
5
71k
RxSwiftは開発をどう変えたか?
ishkawa
12
4.3k
Other Decks in Technology
See All in Technology
アクセラレーターで実現するServiceNow活用の高速化 / ServiceNow World Forum 2026
visional_engineering_and_design
0
120
200人の配信コメント、 全部Jevが仕分けます 〜 マサカリガードを作ってみた 〜
katayamatg
1
500
Don't write the architecture only in the prompt.
neohsu
0
290
Vue で書く 空間コンピューティング、TresJS と WebXR を使ったXR デバイスのフロントエンド開発
hiranuma
0
150
リアクションエンジニア ― 小さなリアクションから、話しかけやすい組織をつくる ―
ryuguyana
1
100
AIに任せられるFlutterコードベースを作る 〜仕様をコードで表し、ハーネスで守る〜
myohei
1
610
構造で守らせる データ・法務・セキュリティ・開発チームとつくる AI時代のデータガバナンスとデータ基盤のリアル
legalontechnologies
PRO
3
720
Agent 活用環境の構築・運用のための AWS サービスのご紹介/agentcore-for-building-and-operating-agents
ren8k
1
310
AIで選択肢が広がった組織設計の理想と現実
konifar
16
7.5k
re:Inventで、人生は変わったのか? / growth-builders-hub-1-reinvent-2026
mhrtech
2
190
Flutter Webを「SDK」として配る — 50以上のゲームを支える埋め込みアーキテクチャ
masayukisuda
0
170
kintoneで在庫管理システムを作るときの要件定義・設計ポイント
kai0707
0
200
Featured
See All Featured
The SEO Collaboration Effect
kristinabergwall1
1
610
GitHub's CSS Performance
jonrohan
1033
470k
From π to Pie charts
rasagy
1
430
Fantastic passwords and where to find them - at NoRuKo
philnash
53
4k
Paper Plane (Part 1)
katiecoart
PRO
2
12k
Mozcon NYC 2025: Stop Losing SEO Traffic
samtorres
1
590
Building an army of robots
kneath
307
47k
[RailsConf 2023 Opening Keynote] The Magic of Rails
eileencodes
31
10k
Color Theory Basics | Prateek | Gurzu
gurzu
1
510
HU Berlin: Industrial-Strength Natural Language Processing with spaCy and Prodigy
inesmontani
PRO
0
780
Marketing to machines
jonoalderson
1
6k
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
620
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