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
【CEDEC2026】『GRANBLUE FANTASY: Relink - Endless Ragnarok』のバトル制作事例 ~最高のキャラゲーを目指して~
cygames
PRO
0
190
plamo-3-translateの開発
pfn
PRO
0
280
AI ネイティブな組織に Gemini Enterprise Agent Platform がなぜ必要なのか
asei
1
170
Breaking the Seal: Static Deobfuscation of Compiled V8 JavaScript Bytecode Malware
hshrzd
0
460
AIが自社のTableauを操作する世界へ:Tableau MCP超入門
tbtykk
0
110
Master Dataグループ紹介資料
sansan33
PRO
1
4.8k
変化の早いClaude Codeを 書籍に落とし込む
oikon48
6
1.3k
JavaScript 研修 (2026)
recruitengineers
PRO
0
260
Service Connect 上のサービスに ECS Service の外側から到達できなかった話
ota1022
1
120
『モンスターストライク』 の運営に伴走する! データ民主化への 解析グループの3つのアプローチ
mixi_engineers
PRO
0
230
Pavlokで始める電撃駆動開発
sgrsn
0
170
Sansan Engineering Unit 紹介資料
sansan33
PRO
1
4.9k
Featured
See All Featured
How to Align SEO within the Product Triangle To Get Buy-In & Support - #RIMC
aleyda
2
1.8k
Amusing Abliteration
ianozsvald
1
240
Navigating Team Friction
lara
192
16k
Designing Experiences People Love
moore
143
24k
The State of eCommerce SEO: How to Win in Today's Products SERPs - #SEOweek
aleyda
2
11k
Done Done
chrislema
186
16k
Exploring the relationship between traditional SERPs and Gen AI search
raygrieselhuber
PRO
2
4.2k
How To Speak Unicorn (iThemes Webinar)
marktimemedia
1
520
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
2.2k
My Coaching Mixtape
mlcsv
0
190
From π to Pie charts
rasagy
0
250
<Decoding/> the Language of Devs - We Love SEO 2024
nikkihalliwell
1
280
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