Skip to content

Commit 771d9b1

Browse files
authored
Controller test (#19)
* Add tests for InfiniteScrollController * Better variable names
1 parent 00dc805 commit 771d9b1

File tree

46 files changed

+627
-178
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+627
-178
lines changed

TestableDesignExample.xcodeproj/project.pbxproj

Lines changed: 184 additions & 104 deletions
Large diffs are not rendered by default.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import UIKit
2+
3+
4+
5+
protocol InfiniteScrollTriggerProtocol {
6+
func shouldLoadY(
7+
contentOffset: CGPoint,
8+
contentSize: CGSize,
9+
scrollViewSize: CGSize
10+
) -> Bool
11+
}
12+
13+
14+
15+
class InfiniteScrollThresholdTrigger: InfiniteScrollTriggerProtocol {
16+
private let threshold: CGFloat
17+
18+
19+
init(basedOn threshold: CGFloat) {
20+
self.threshold = threshold
21+
}
22+
23+
24+
func shouldLoadY(
25+
contentOffset: CGPoint,
26+
contentSize: CGSize,
27+
scrollViewSize: CGSize
28+
) -> Bool {
29+
let offsetY = contentOffset.y
30+
let contentHeight = contentSize.height
31+
let visibleHeight = scrollViewSize.height
32+
33+
return offsetY >= contentHeight
34+
- visibleHeight
35+
- threshold
36+
}
37+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import UIKit
2+
@testable import TestableDesignExample
3+
4+
5+
6+
class InfiniteScrollTriggerStub: InfiniteScrollTriggerProtocol {
7+
var nextResult: Bool
8+
9+
10+
init(firstResult: Bool) {
11+
self.nextResult = firstResult
12+
}
13+
14+
15+
func shouldLoadY(contentOffset: CGPoint, contentSize: CGSize, scrollViewSize: CGSize) -> Bool {
16+
return self.nextResult
17+
}
18+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import XCTest
2+
import UIKit
3+
@testable import TestableDesignExample
4+
5+
6+
class InfiniteScrollTriggerTests: XCTestCase {
7+
private struct TestCase {
8+
let threshold: CGFloat
9+
let scrollView: CGSize
10+
let contentView: CGSize
11+
let contentOffset: CGPoint
12+
let expected: Bool
13+
}
14+
15+
16+
func testShouldLoad() {
17+
let testCases: [UInt: TestCase] = [
18+
#line: TestCase(
19+
threshold: 100,
20+
scrollView: CGSize(width: 100, height: 200),
21+
contentView: CGSize(width: 100, height: 400),
22+
contentOffset: CGPoint(x: 0, y: 0),
23+
expected: false
24+
),
25+
26+
#line: TestCase(
27+
threshold: 100,
28+
scrollView: CGSize(width: 100, height: 200),
29+
contentView: CGSize(width: 100, height: 400),
30+
contentOffset: CGPoint(x: 0, y: 300),
31+
expected: true
32+
),
33+
]
34+
35+
36+
testCases.forEach { entry in
37+
let (line, testCase) = entry
38+
39+
let trigger = InfiniteScrollThresholdTrigger(
40+
basedOn: testCase.threshold
41+
)
42+
43+
let actual = trigger.shouldLoadY(
44+
contentOffset: testCase.contentOffset,
45+
contentSize: testCase.contentView,
46+
scrollViewSize: testCase.scrollView
47+
)
48+
49+
XCTAssertEqual(actual, testCase.expected, line: line)
50+
}
51+
}
52+
}
53+

TestableDesignExample/MvcArchitecture/Shared/Navigator/RootNavigator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class RootNavigator: RootNavigatorProtocol {
3333
let api = GitHubApiClient(basedOn: GitHubApiEndpointBaseUrl.gitHubCom)
3434
let bag = Bag(api: api)
3535

36-
let stargazerModel = StargazerModel.create(
36+
let stargazerModel = StargazersModel.create(
3737
requestingElementCountPerPage: PerformanceParameter.numberOfStargazersPerPage,
3838
fetchingPageVia: StargazerRepository(
3939
for: gitHubRepository,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import XCTest
2+
@testable import TestableDesignExample
3+
4+
5+
class RootNavigatorTests: XCTestCase {
6+
func testNavigateToRoot() {
7+
let spy = RootViewControllerHolderSpy()
8+
let rootNavigator = RootNavigator(using: spy)
9+
10+
rootNavigator.navigateToRoot()
11+
12+
XCTAssertEqual(spy.callArgs.count, 1)
13+
}
14+
}

TestableDesignExample/MvcArchitecture/Stargazers/Controller/StargazersInfiniteScrollController.swift

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,34 +10,36 @@ protocol StargazersInfiniteScrollControllerProtocol {}
1010

1111
class StargazersInfiniteScrollController: StargazersInfiniteScrollControllerProtocol {
1212
private let scrollView: UIScrollView
13-
private let model: StargazerModelProtocol
13+
private let model: StargazersModelProtocol
14+
private let trigger: InfiniteScrollTriggerProtocol
1415
private let disposeBag = RxSwift.DisposeBag()
16+
internal var didHandle = {}
1517

1618

1719
init(
1820
watching scrollView: UIScrollView,
19-
notifying model: StargazerModelProtocol
21+
determiningBy trigger: InfiniteScrollTriggerProtocol,
22+
notifying model: StargazersModelProtocol
2023
) {
2124
self.scrollView = scrollView
2225
self.model = model
26+
self.trigger = trigger
2327

2428
self.scrollView.rx
2529
.didScroll
2630
.asDriver()
2731
.drive(onNext: { [weak self] _ in
2832
guard let this = self else { return }
2933

30-
let offsetY = this.scrollView.contentOffset.y
31-
let contentHeight = this.scrollView.contentSize.height
32-
let visibleHeight = this.scrollView.bounds.height
33-
34-
let isReachingThreshold = offsetY >= contentHeight
35-
- visibleHeight
36-
- PerformanceParameter.stargazersInfiniteScrollThreshold
37-
38-
if isReachingThreshold {
34+
if this.trigger.shouldLoadY(
35+
contentOffset: this.scrollView.contentOffset,
36+
contentSize: this.scrollView.contentSize,
37+
scrollViewSize: this.scrollView.bounds.size
38+
) {
3939
this.model.fetchNext()
4040
}
41+
42+
this.didHandle()
4143
})
4244
.disposed(by: self.disposeBag)
4345
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import XCTest
2+
@testable import TestableDesignExample
3+
4+
5+
class StargazersInfiniteScrollControllerTests: XCTestCase {
6+
func testTrigger() {
7+
let scrollView = self.createScrollView()
8+
9+
awaitUntilVisible(on: self, testing: scrollView) { fulfill in
10+
let spy = StargazersModelSpy()
11+
12+
let controller = StargazersInfiniteScrollController(
13+
watching: scrollView,
14+
determiningBy: InfiniteScrollTriggerStub(
15+
firstResult: true
16+
),
17+
notifying: spy
18+
)
19+
20+
controller.didHandle = {
21+
XCTAssertEqual(spy.callArgs.count, 1)
22+
fulfill()
23+
}
24+
25+
scrollView.setContentOffset(
26+
CGPoint(x: 0, y: 200),
27+
animated: false
28+
)
29+
}
30+
}
31+
32+
33+
func testNotTrigger() {
34+
let scrollView = self.createScrollView()
35+
36+
awaitUntilVisible(on: self, testing: scrollView) { fulfill in
37+
let spy = StargazersModelSpy()
38+
39+
let controller = StargazersInfiniteScrollController(
40+
watching: scrollView,
41+
determiningBy: InfiniteScrollTriggerStub(
42+
firstResult: false
43+
),
44+
notifying: spy
45+
)
46+
47+
controller.didHandle = {
48+
XCTAssertEqual(spy.callArgs.count, 0)
49+
fulfill()
50+
}
51+
52+
scrollView.setContentOffset(
53+
CGPoint(x: 0, y: 1),
54+
animated: false
55+
)
56+
}
57+
}
58+
59+
60+
private func createScrollView() -> UIScrollView {
61+
let screenWidth: CGFloat = 100
62+
let screenHeight = PerformanceParameter.stargazersInfiniteScrollThreshold
63+
64+
let views = UIScrollView.create(
65+
sizeOf: (
66+
scrollView: CGSize(width: screenWidth, height: screenHeight),
67+
contentView: CGSize(width: screenWidth, height: screenHeight * 2)
68+
),
69+
scrolledAt: .zero
70+
)
71+
72+
return views.scrollView
73+
}
74+
}

TestableDesignExample/MvcArchitecture/Stargazers/Controller/StargazersRefreshController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ protocol StargazersRefreshControllerProtocol {}
1010

1111
class StargazersRefreshController: StargazersRefreshControllerProtocol {
1212
private let refreshController: UIRefreshControl
13-
private let model: StargazerModelProtocol
13+
private let model: StargazersModelProtocol
1414
private let disposeBag = RxSwift.DisposeBag()
1515

1616

1717
init(
1818
watching refreshController: UIRefreshControl,
19-
notifying model: StargazerModelProtocol
19+
notifying model: StargazersModelProtocol
2020
) {
2121
self.refreshController = refreshController
2222
self.model = model

TestableDesignExample/MvcArchitecture/Stargazers/Model/StargazersModel.swift

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import RxSwift
33

44

55

6-
protocol StargazerModelProtocol {
7-
var didChange: RxSwift.Observable<StargazerModelState> { get }
8-
var currentState: StargazerModelState { get }
6+
protocol StargazersModelProtocol {
7+
var didChange: RxSwift.Observable<StargazersModelState> { get }
8+
var currentState: StargazersModelState { get }
99

1010
func fetchNext()
1111
func fetchPrevious()
@@ -15,28 +15,42 @@ protocol StargazerModelProtocol {
1515

1616

1717

18-
enum StargazerModelState {
19-
case fetched(stargazers: [GitHubUser], error: StargazerModelError?)
18+
enum StargazersModelState {
19+
case fetched(stargazers: [GitHubUser], error: FailureReason?)
2020
case fetching(previousStargazers: [GitHubUser])
2121

2222

23-
static func from(pagingModelState: PagingModelState<GitHubUser>) -> StargazerModelState {
23+
static func from(pagingModelState: PagingModelState<GitHubUser>) -> StargazersModelState {
2424
switch pagingModelState {
2525
case let .fetching(beforeElements: stargazers):
2626
return .fetching(previousStargazers: stargazers)
2727

2828
case let .fetched(elements: stargazers, error: error):
2929
return .fetched(
3030
stargazers: stargazers,
31-
error: StargazerModelError.from(pagingModelError: error)
31+
error: FailureReason.from(pagingModelError: error)
3232
)
3333
}
3434
}
35+
36+
37+
enum FailureReason: Error {
38+
case apiError(debugInfo: String)
39+
40+
41+
static func from(pagingModelError: PagingModelState<GitHubUser>.ModelError?) -> FailureReason? {
42+
guard let pagingModelError = pagingModelError else {
43+
return nil
44+
}
45+
46+
return .apiError(debugInfo: "\(pagingModelError)")
47+
}
48+
}
3549
}
3650

3751

38-
extension StargazerModelState: Equatable {
39-
static func ==(lhs: StargazerModelState, rhs: StargazerModelState) -> Bool {
52+
extension StargazersModelState: Equatable {
53+
static func ==(lhs: StargazersModelState, rhs: StargazersModelState) -> Bool {
4054
switch (lhs, rhs) {
4155
case let (.fetched(stargazers: ls, error: le), .fetched(stargazers: rs, error: re)):
4256
return ls == rs && le == re
@@ -50,22 +64,8 @@ extension StargazerModelState: Equatable {
5064

5165

5266

53-
enum StargazerModelError: Error {
54-
case apiError(debugInfo: String)
55-
56-
57-
static func from(pagingModelError: PagingModelState<GitHubUser>.ModelError?) -> StargazerModelError? {
58-
guard let pagingModelError = pagingModelError else {
59-
return nil
60-
}
61-
62-
return .apiError(debugInfo: "\(pagingModelError)")
63-
}
64-
}
65-
66-
67-
extension StargazerModelError: Equatable {
68-
static func ==(lhs: StargazerModelError, rhs: StargazerModelError) -> Bool {
67+
extension StargazersModelState.FailureReason: Equatable {
68+
static func ==(lhs: StargazersModelState.FailureReason, rhs: StargazersModelState.FailureReason) -> Bool {
6969
switch (lhs, rhs) {
7070
case (.apiError, .apiError):
7171
return true
@@ -75,19 +75,19 @@ extension StargazerModelError: Equatable {
7575

7676

7777

78-
class StargazerModel: StargazerModelProtocol {
78+
class StargazersModel: StargazersModelProtocol {
7979
private let pagingModel: AnyPagingModel<GitHubUser>
8080

8181

82-
var didChange: Observable<StargazerModelState> {
82+
var didChange: Observable<StargazersModelState> {
8383
return self.pagingModel
8484
.didChange
85-
.map { StargazerModelState.from(pagingModelState: $0) }
85+
.map { StargazersModelState.from(pagingModelState: $0) }
8686
}
8787

8888

89-
var currentState: StargazerModelState {
90-
return StargazerModelState.from(
89+
var currentState: StargazersModelState {
90+
return StargazersModelState.from(
9191
pagingModelState: self.pagingModel.currentState
9292
)
9393
}
@@ -123,8 +123,8 @@ class StargazerModel: StargazerModelProtocol {
123123
static func create<PageRepository: PageRepositoryProtocol> (
124124
requestingElementCountPerPage elementCount: Int,
125125
fetchingPageVia pageRepository: PageRepository
126-
) -> StargazerModel where PageRepository.Element == GitHubUser {
127-
return StargazerModel(
126+
) -> StargazersModel where PageRepository.Element == GitHubUser {
127+
return StargazersModel(
128128
pagingBy: PagingModel(
129129
fetchingPageVia: pageRepository,
130130
detectingPageEndBy: PageElementCountStrategy(

0 commit comments

Comments
 (0)