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.5k
JSON-RPC on APIKit
ishkawa
5
70k
RxSwiftは開発をどう変えたか?
ishkawa
12
4.2k
Other Decks in Technology
See All in Technology
老害フォレンジッカーはAI羊の夢を見るか?
tadmaddad
0
350
도구에서 동료까지: 10년차 AI 스타트업의 AI 적응기
inureyes
PRO
1
280
SO-101×VLAによる3色キューブのピック&プレース
abeja
0
220
システム思考で問題に対処する
yussak
0
360
20260817_生成AIの動向と中学校・高等学校での活用を考える_v1.00_公開用
doradora09
PRO
0
130
自宅NWにISR4331を導入してみた話
okaits
0
120
Go 1.27 の標準パッケージに uuid が入った!のでいろいろ喋る / go_127_std_go_uuid
convto
3
540
LLM・AIエージェントシステムベストプラクティス
shibuiwilliam
6
1.6k
研究開発部の紹介 / Sansan R&D Profile
sansan33
PRO
4
24k
現場回帰したデータエンジニアが考える AI 時代のキャリア開発 / Career Development in the Age of AI Perspectives from a Hands-on Data Engineer
medley
0
280
MIXIで活躍できるエンジニアを 若手社員目線で考えてみる
mixi_engineers
PRO
0
110
猫付きpingコマンドを自作
uyuki234
0
270
Featured
See All Featured
Why Your Marketing Sucks and What You Can Do About It - Sophie Logan
marketingsoph
0
390
Neural Spatial Audio Processing for Sound Field Analysis and Control
skoyamalab
0
410
Making the Leap to Tech Lead
cromwellryan
135
10k
From Legacy to Launchpad: Building Startup-Ready Communities
dugsong
0
300
XXLCSS - How to scale CSS and keep your sanity
sugarenia
249
1.3M
Winning Ecommerce Organic Search in an AI Era - #searchnstuff2025
aleyda
1
2.1k
JAMstack: Web Apps at Ludicrous Speed - All Things Open 2022
reverentgeek
1
570
How to build an LLM SEO readiness audit: a practical framework
nmsamuel
1
870
Optimizing for Happiness
mojombo
378
71k
Facilitating Awesome Meetings
lara
57
7.1k
10 Git Anti Patterns You Should be Aware of
lemiorhan
PRO
659
62k
WENDY [Excerpt]
tessaabrams
11
39k
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