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
260
アプリを起動せずにアプリを開発して品質と生産性を上げる
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
スプシの手作業からの解脱 ~更新はClaude Codeに、見せ方と使い方はGASアプリに任せる~
kentarofujii
0
180
AI駆動で「他アプリの音を加工できる」アプリを作った
satomasahiro2005
0
190
freeeにおける AI を用いた ブラウザ動作確認の自動化の取り組み
freee
PRO
0
130
フィンテック養成勉強会#68
finengine
0
210
『SAR衛星データ解析入門』のご紹介とその応用
syuchimu
0
140
ストレスなくAIと付き合いたい!こんなときどうしてる?
namiki64
4
670
PHPカンファレンス関西の招待状 / Invitation to PHP Conference Kansai
ykanoh
0
220
From Developer Experience to Agentic Experience: The Next Evolution of APIs
benjagm
0
110
DBについての基礎説明(MySQL編) - コロプラ 2026年度 新卒研修
colopl
0
160
生成AI時代、PMは事業責任者に進化するともっと面白い〜PMだからこそ事業構造を動かせる時代に〜
kakehashi
PRO
0
300
[JAWS-UG彩の国埼玉#12]そろそろ準備する?re:Invent2026をやさしく解説 / reinvent2026-sainokuni
sh_fk2
1
390
AI コーディングと人間の認知スループット / CTO Night & Day 2026 Morning Session / AI Coding and Human Cognitive Throughput
prog893
5
1.5k
Featured
See All Featured
Building Adaptive Systems
keathley
44
3.3k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.4k
Why You Should Never Use an ORM
jnunemaker
PRO
61
10k
Git: the NoSQL Database
bkeepers
PRO
433
67k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
How to Talk to Developers About Accessibility
jct
2
590
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
2
950
What Being in a Rock Band Can Teach Us About Real World SEO
427marketing
0
1.1k
AI Search: Implications for SEO and How to Move Forward - #ShenzhenSEOConference
aleyda
1
1.4k
KATA
mclloyd
PRO
35
16k
The untapped power of vector embeddings
frankvandijk
2
1.9k
Joys of Absence: A Defence of Solitary Play
codingconduct
1
550
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