-
Notifications
You must be signed in to change notification settings - Fork 492
Expand file tree
/
Copy pathFeedAcceptanceTests.swift
More file actions
236 lines (184 loc) · 7.41 KB
/
FeedAcceptanceTests.swift
File metadata and controls
236 lines (184 loc) · 7.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
//
// Copyright © Essential Developer. All rights reserved.
//
import XCTest
import EssentialFeed
import EssentialFeediOS
@testable import EssentialApp
@MainActor
class FeedAcceptanceTests: XCTestCase {
func test_onLaunch_displaysRemoteFeedWhenCustomerHasConnectivity() throws {
let store = try CoreDataFeedStore.empty
let feed = try launch(httpClient: .online(response), store: store)
XCTAssertEqual(feed.numberOfRenderedFeedImageViews(), 2)
XCTAssertEqual(feed.renderedFeedImageData(at: 0), makeImageData0())
XCTAssertEqual(feed.renderedFeedImageData(at: 1), makeImageData1())
XCTAssertTrue(feed.canLoadMoreFeed)
try store.withWaitingChanges {
feed.simulateLoadMoreFeedAction()
}
XCTAssertEqual(feed.numberOfRenderedFeedImageViews(), 3)
XCTAssertEqual(feed.renderedFeedImageData(at: 0), makeImageData0())
XCTAssertEqual(feed.renderedFeedImageData(at: 1), makeImageData1())
XCTAssertEqual(feed.renderedFeedImageData(at: 2), makeImageData2())
XCTAssertTrue(feed.canLoadMoreFeed)
try store.withWaitingChanges {
feed.simulateLoadMoreFeedAction()
}
XCTAssertEqual(feed.numberOfRenderedFeedImageViews(), 3)
XCTAssertEqual(feed.renderedFeedImageData(at: 0), makeImageData0())
XCTAssertEqual(feed.renderedFeedImageData(at: 1), makeImageData1())
XCTAssertEqual(feed.renderedFeedImageData(at: 2), makeImageData2())
XCTAssertFalse(feed.canLoadMoreFeed)
}
func test_onLaunch_displaysCachedRemoteFeedWhenCustomerHasNoConnectivity() throws {
let sharedStore = try CoreDataFeedStore.empty
let onlineFeed = try launch(httpClient: .online(response), store: sharedStore)
onlineFeed.simulateFeedImageViewVisible(at: 0)
onlineFeed.simulateFeedImageViewVisible(at: 1)
try sharedStore.withWaitingChanges {
onlineFeed.simulateLoadMoreFeedAction()
}
onlineFeed.simulateFeedImageViewVisible(at: 2)
let offlineFeed = try launch(httpClient: .offline, store: sharedStore)
XCTAssertEqual(offlineFeed.numberOfRenderedFeedImageViews(), 3)
XCTAssertEqual(offlineFeed.renderedFeedImageData(at: 0), makeImageData0())
XCTAssertEqual(offlineFeed.renderedFeedImageData(at: 1), makeImageData1())
XCTAssertEqual(offlineFeed.renderedFeedImageData(at: 2), makeImageData2())
}
func test_onLaunch_displaysEmptyFeedWhenCustomerHasNoConnectivityAndNoCache() throws {
let feed = try launch(httpClient: .offline, store: .empty)
XCTAssertEqual(feed.numberOfRenderedFeedImageViews(), 0)
}
func test_onEnteringBackground_deletesExpiredFeedCache() throws {
let store = try CoreDataFeedStore.withExpiredFeedCache
enterBackground(with: store)
XCTAssertNil(try store.retrieve(), "Expected to delete expired cache")
}
func test_onEnteringBackground_keepsNonExpiredFeedCache() throws {
let store = try CoreDataFeedStore.withNonExpiredFeedCache
enterBackground(with: store)
XCTAssertNotNil(try store.retrieve(), "Expected to keep non-expired cache")
}
func test_onFeedImageSelection_displaysComments() throws {
let comments = try showCommentsForFirstImage()
XCTAssertEqual(comments.numberOfRenderedComments(), 1)
XCTAssertEqual(comments.commentMessage(at: 0), makeCommentMessage())
}
// MARK: - Helpers
private func launch(
httpClient: HTTPClientStub = .offline,
store: CoreDataFeedStore
) throws -> ListViewController {
let sut = SceneDelegate(httpClient: httpClient, store: store)
let dummyScene = try XCTUnwrap((UIWindowScene.self as NSObject.Type).init() as? UIWindowScene)
sut.window = UIWindow(windowScene: dummyScene)
sut.window?.frame = CGRect(x: 0, y: 0, width: 390, height: 1)
sut.configureWindow()
let nav = sut.window?.rootViewController as? UINavigationController
let vc = nav?.topViewController as! ListViewController
vc.simulateAppearance()
return vc
}
private func enterBackground(with store: CoreDataFeedStore) {
let sut = SceneDelegate(httpClient: HTTPClientStub.offline, store: store)
sut.sceneWillResignActive(UIApplication.shared.connectedScenes.first!)
}
private func showCommentsForFirstImage() throws -> ListViewController {
let feed = try launch(httpClient: .online(response), store: .empty)
feed.simulateTapOnFeedImage(at: 0)
RunLoop.current.run(until: Date())
let nav = feed.navigationController
let vc = nav?.topViewController as! ListViewController
vc.simulateAppearance()
return vc
}
private func response(for url: URL) -> (Data, HTTPURLResponse) {
let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)!
return (makeData(for: url), response)
}
private func makeData(for url: URL) -> Data {
switch url.path {
case "/image-0": return makeImageData0()
case "/image-1": return makeImageData1()
case "/image-2": return makeImageData2()
case "/essential-feed/v1/feed" where url.query?.contains("after_id") == false:
return makeFirstFeedPageData()
case "/essential-feed/v1/feed" where url.query?.contains("after_id=A28F5FE3-27A7-44E9-8DF5-53742D0E4A5A") == true:
return makeSecondFeedPageData()
case "/essential-feed/v1/feed" where url.query?.contains("after_id=166FCDD7-C9F4-420A-B2D6-CE2EAFA3D82F") == true:
return makeLastEmptyFeedPageData()
case "/essential-feed/v1/image/2AB2AE66-A4B7-4A16-B374-51BBAC8DB086/comments":
return makeCommentsData()
default:
return Data()
}
}
private func makeImageData0() -> Data { UIImage.make(withColor: .red).pngData()! }
private func makeImageData1() -> Data { UIImage.make(withColor: .green).pngData()! }
private func makeImageData2() -> Data { UIImage.make(withColor: .blue).pngData()! }
private func makeFirstFeedPageData() -> Data {
return try! JSONSerialization.data(withJSONObject: ["items": [
["id": "2AB2AE66-A4B7-4A16-B374-51BBAC8DB086", "image": "http://feed.com/image-0"],
["id": "A28F5FE3-27A7-44E9-8DF5-53742D0E4A5A", "image": "http://feed.com/image-1"]
]])
}
private func makeSecondFeedPageData() -> Data {
return try! JSONSerialization.data(withJSONObject: ["items": [
["id": "166FCDD7-C9F4-420A-B2D6-CE2EAFA3D82F", "image": "http://feed.com/image-2"],
]])
}
private func makeLastEmptyFeedPageData() -> Data {
try! JSONSerialization.data(withJSONObject: ["items": [[String: Any]]()])
}
private func makeCommentsData() -> Data {
try! JSONSerialization.data(withJSONObject: ["items": [
[
"id": UUID().uuidString,
"message": makeCommentMessage(),
"created_at": "2020-05-20T11:24:59+0000",
"author": [
"username": "a username"
]
] as [String: Any],
]])
}
private func makeCommentMessage() -> String {
"a message"
}
}
@MainActor
extension CoreDataFeedStore {
private struct Timeout: Error {}
func withWaitingChanges(_ action: () -> Void, timeout: TimeInterval = 1) throws {
let state = try retrieve()?.timestamp
action()
let maxDate = Date() + timeout
while Date() <= maxDate {
if try retrieve()?.timestamp != state {
return
}
RunLoop.current.run(until: Date())
}
throw Timeout()
}
static var empty: CoreDataFeedStore {
get throws {
try CoreDataFeedStore(storeURL: URL(fileURLWithPath: "/dev/null"), contextQueue: .main)
}
}
static var withExpiredFeedCache: CoreDataFeedStore {
get throws {
let store = try CoreDataFeedStore.empty
try store.insert([], timestamp: .distantPast)
return store
}
}
static var withNonExpiredFeedCache: CoreDataFeedStore {
get throws {
let store = try CoreDataFeedStore.empty
try store.insert([], timestamp: Date())
return store
}
}
}