Skip to content

Commit

Permalink
Move get posts list stub to an XCTestCase extension
Browse files Browse the repository at this point in the history
  • Loading branch information
crazytonyli committed Nov 7, 2023
1 parent a3f6771 commit 78c3311
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 113 deletions.
4 changes: 4 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,7 @@
4AD5656F28E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; };
4AD5657028E413160054C676 /* Blog+History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5656E28E413160054C676 /* Blog+History.swift */; };
4AD5657228E543A30054C676 /* BlogQueryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD5657128E543A30054C676 /* BlogQueryTests.swift */; };
4AD862E52AFAEF1700A07557 /* PostsListAPIStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */; };
4AEF2DD929A84B2C00345734 /* ReaderSiteServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AEF2DD829A84B2C00345734 /* ReaderSiteServiceTests.swift */; };
4AFB1A812A9C08CE007CE165 /* StoppableProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */; };
4AFB1A822A9C08CE007CE165 /* StoppableProgressIndicatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */; };
Expand Down Expand Up @@ -6924,6 +6925,7 @@
4AD5656B28E3D0670054C676 /* ReaderPost+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReaderPost+Helper.swift"; sourceTree = "<group>"; };
4AD5656E28E413160054C676 /* Blog+History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Blog+History.swift"; sourceTree = "<group>"; };
4AD5657128E543A30054C676 /* BlogQueryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlogQueryTests.swift; sourceTree = "<group>"; };
4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostsListAPIStub.swift; sourceTree = "<group>"; };
4AEF2DD829A84B2C00345734 /* ReaderSiteServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderSiteServiceTests.swift; sourceTree = "<group>"; };
4AFB1A802A9C08CE007CE165 /* StoppableProgressIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoppableProgressIndicatorView.swift; sourceTree = "<group>"; };
4AFB8FBE2824999400A2F4B2 /* ContextManager+Helpers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ContextManager+Helpers.swift"; sourceTree = "<group>"; };
Expand Down Expand Up @@ -13616,6 +13618,7 @@
FE6BB1452932289B001E5F7A /* ContentMigrationCoordinatorTests.swift */,
0C896DE62A3A832B00D7D4E7 /* SiteVisibilityTests.swift */,
0CA10FA42ADB286300CE75AC /* StringRankedSearchTests.swift */,
4AD862E42AFAEF1700A07557 /* PostsListAPIStub.swift */,
93B853211B44165B0064FE72 /* Analytics */,
DC06DFF727BD52A100969974 /* BackgroundTasks */,
FE32E7EF284496F500744D80 /* Blogging Prompts */,
Expand Down Expand Up @@ -23560,6 +23563,7 @@
D848CC0320FF04FA00A9038F /* FormattableUserContentTests.swift in Sources */,
5948AD111AB73D19006E8882 /* WPAppAnalyticsTests.m in Sources */,
0C8FC9AA2A8C57000059DCE4 /* ItemProviderMediaExporterTests.swift in Sources */,
4AD862E52AFAEF1700A07557 /* PostsListAPIStub.swift in Sources */,
0A69300B28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift in Sources */,
0CA10FA52ADB286300CE75AC /* StringRankedSearchTests.swift in Sources */,
FF8032661EE9E22200861F28 /* MediaProgressCoordinatorTests.swift in Sources */,
Expand Down
116 changes: 3 additions & 113 deletions WordPress/WordPressTest/PostRepositoryPostsListTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class PostRepositoryPostsListTests: CoreDataTestCase {

func testPagination() async throws {
// Given there are 15 published posts on the site
try await preparePostsList(type: "post", total: 15)
stubGetPostsList(type: "post", total: 15)

// When fetching all of the posts
let firstPage = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 10, in: blogID)
Expand All @@ -44,7 +44,7 @@ class PostRepositoryPostsListTests: CoreDataTestCase {

func testSearching() async throws {
// Given there are 15 published posts on the site
try await preparePostsList(type: "post", total: 15)
stubGetPostsList(type: "post", total: 15)

// When fetching all of the posts
let _ = try await repository.paginate(type: Post.self, statuses: [.publish], offset: 0, number: 15, in: blogID)
Expand All @@ -63,7 +63,7 @@ class PostRepositoryPostsListTests: CoreDataTestCase {
}

func testFetchingAllPages() async throws {
try await preparePostsList(type: "page", total: 1_120)
stubGetPostsList(type: "page", total: 1_120)

let pages = try await repository.fetchAllPages(statuses: [], in: blogID).value
XCTAssertEqual(pages.count, 1_120)
Expand All @@ -75,116 +75,6 @@ class PostRepositoryPostsListTests: CoreDataTestCase {

}

// MARK: - Tests that ensure the `preparePostsList` works as expected

extension PostRepositoryPostsListTests {

func testPostsListStubReturnPostsAsRequested() async throws {
try await preparePostsList(type: "post", total: 20)

var result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 10, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 10)

result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 20, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)

result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)
}

func testPostsListStubReturnPostsAtCorrectPosition() async throws {
try await preparePostsList(type: "post", total: 20)

let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)

var result = try await repository.paginate(type: Post.self, statuses: [], offset: 0, number: 5, in: blogID)
XCTAssertEqual(result, Array(all[0..<5]))

result = try await repository.paginate(type: Post.self, statuses: [], offset: 3, number: 2, in: blogID)
XCTAssertEqual(result, [all[3], all[4]])
}

func testPostsListStubReturnPostsSearch() async throws {
try await preparePostsList(type: "post", total: 10)

let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)

var result = try await repository.search(type: Post.self, input: "1", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result, [all[0]])

result = try await repository.search(type: Post.self, input: "2", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result, [all[1]])
}

func testPostsListStubReturnDefaultNumberOfPosts() async throws {
try await preparePostsList(type: "post", total: 100)

let result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 0, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)
}

}

private extension PostRepositoryPostsListTests {
/// This is a helper function to create HTTP stubs for fetching posts(GET /sites/%s/posts) requests.
///
/// The returned fake posts have only basic properties. The stubs ensure post id is unique and starts from 1.
/// But it does not promise the returned posts match the request filters (like status).
///
/// You can use the `update` closure to update the returned posts if needed.
///
/// Here are the supported features:
/// - Pagination. The stubs simulates `total` number of posts in the site, to handle paginated request accordingly.
/// - Search, but limited. Search is based on title. All fake posts have a title like "Random Post - [post-id]", where post id starts from 1. So, search "1" returns the posts whose id has "1" in it (1, 1x, x1, and so on).
///
/// Here are unsupported features:
/// - Order. The sorting related arguments are ignored.
/// - Filter by status. The status argument is ignored.
func preparePostsList(type: String, total: Int, update: ((inout [String: Any]) -> Void)? = nil) async throws {
let allPosts = (1...total).map { id -> [String: Any] in
[
"ID": id,
"title": "Random Post - \(id)",
"content": "This is a test.",
"status": BasePost.Status.publish.rawValue,
"type": type
]
}

let siteID = try await contextManager.performQuery { try XCTUnwrap($0.existingObject(with: self.blogID).dotComID) }
stub(condition: isMethodGET() && pathMatches("/sites/\(siteID)/posts", options: [])) { request in
let queryItems = URLComponents(url: request.url!, resolvingAgainstBaseURL: true)?.queryItems ?? []

var result = allPosts

if let search = queryItems.first(where: { $0.name == "search" })?.value {
result = result.filter {
let title = $0["title"] as? String
return title?.contains(search) == true
}
}

var number = (queryItems.first { $0.name == "number" }?.value.flatMap { Int($0) }) ?? 0
number = number == 0 ? 20 : number // The REST API uses the default value 20 when number is 0.
let offset = (queryItems.first { $0.name == "offset" }?.value.flatMap { Int($0) }) ?? 0
let upperBound = number == 0 ? result.endIndex : max(offset, offset + number - 1)
let allowed = 0..<result.count
let range = (offset..<(upperBound + 1)).clamped(to: allowed)

let response: [String: Any] = [
"found": result.count,
"posts": result[range].map { post in
var json = post
update?(&json)
return json
}
]

return HTTPStubsResponse(jsonObject: response, statusCode: 200, headers: nil)
}
}
}

extension CoreDataTestCase {

func signIn() async throws -> NSManagedObjectID {
Expand Down
143 changes: 143 additions & 0 deletions WordPress/WordPressTest/PostsListAPIStub.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import Foundation
import XCTest
import OHHTTPStubs

@testable import WordPress

extension XCTestCase {
/// This is a helper function to create HTTP stubs for fetching posts(GET /sites/%s/posts) requests.
///
/// The returned fake posts have only basic properties. The stubs ensure post id is unique and starts from 1.
/// But it does not promise the returned posts match the request filters (like status).
///
/// You can use the `update` closure to update the returned posts if needed.
///
/// Here are the supported features:
/// - Pagination. The stubs simulates `total` number of posts in the site, to handle paginated request accordingly.
/// - Search, but limited. Search is based on title. All fake posts have a title like "Random Post - [post-id]", where post id starts from 1. So, search "1" returns the posts whose id has "1" in it (1, 1x, x1, and so on).
///
/// Here are unsupported features:
/// - Order. The sorting related arguments are ignored.
/// - Filter by status. The status argument is ignored.
func stubGetPostsList(type: String, total: Int, update: ((inout [String: Any]) -> Void)? = nil) {
let allPosts = (1...total).map { id -> [String: Any] in
[
"ID": id,
"title": "Random Post - \(id)",
"content": "This is a test.",
"status": BasePost.Status.publish.rawValue,
"type": type
]
}

let handle = stub(condition: isMethodGET() && pathMatches(#"/sites/\d+/posts"#, options: [])) { request in
let queryItems = URLComponents(url: request.url!, resolvingAgainstBaseURL: true)?.queryItems ?? []

var result = allPosts

if let search = queryItems.first(where: { $0.name == "search" })?.value {
result = result.filter {
let title = $0["title"] as? String
return title?.contains(search) == true
}
}

var number = (queryItems.first { $0.name == "number" }?.value.flatMap { Int($0) }) ?? 0
number = number == 0 ? 20 : number // The REST API uses the default value 20 when number is 0.
let offset = (queryItems.first { $0.name == "offset" }?.value.flatMap { Int($0) }) ?? 0
let upperBound = number == 0 ? result.endIndex : max(offset, offset + number - 1)
let allowed = 0..<result.count
let range = (offset..<(upperBound + 1)).clamped(to: allowed)

let response: [String: Any] = [
"found": result.count,
"posts": result[range].map { post in
var json = post
update?(&json)
return json
}
]

return HTTPStubsResponse(jsonObject: response, statusCode: 200, headers: nil)
}

addTeardownBlock {
HTTPStubs.removeStub(handle)
}
}

func stubGetPostsListWithServerError() {
let handle = stub(condition: isMethodGET() && pathMatches(#"/sites/\d+/posts"#, options: [])) { _ in
HTTPStubsResponse(data: Data(), statusCode: 500, headers: nil)
}
addTeardownBlock {
HTTPStubs.removeStub(handle)
}
}
}

// MARK: - Tests that ensure the `preparePostsList` works as expected

class PostsListAPIStubTests: CoreDataTestCase {

var repository: PostRepository!
var blogID: TaggedManagedObjectID<Blog>!

override func setUp() async throws {
repository = PostRepository(coreDataStack: contextManager)

let loggedIn = try await signIn()
blogID = try await contextManager.performAndSave {
let blog = try BlogBuilder($0)
.with(dotComID: 42)
.withAccount(id: loggedIn)
.build()
return TaggedManagedObjectID(blog)
}
}

func testPostsListStubReturnPostsAsRequested() async throws {
stubGetPostsList(type: "post", total: 20)

var result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 10, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 10)

result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 20, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)

result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)
}

func testPostsListStubReturnPostsAtCorrectPosition() async throws {
stubGetPostsList(type: "post", total: 20)

let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)

var result = try await repository.paginate(type: Post.self, statuses: [], offset: 0, number: 5, in: blogID)
XCTAssertEqual(result, Array(all[0..<5]))

result = try await repository.paginate(type: Post.self, statuses: [], offset: 3, number: 2, in: blogID)
XCTAssertEqual(result, [all[3], all[4]])
}

func testPostsListStubReturnPostsSearch() async throws {
stubGetPostsList(type: "post", total: 10)

let all = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 30, orderBy: .byDate, descending: true, in: blogID)

var result = try await repository.search(type: Post.self, input: "1", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result, [all[0]])

result = try await repository.search(type: Post.self, input: "2", statuses: [], tag: nil, offset: 0, limit: 1, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result, [all[1]])
}

func testPostsListStubReturnDefaultNumberOfPosts() async throws {
stubGetPostsList(type: "post", total: 100)

let result = try await repository.search(type: Post.self, input: nil, statuses: [], tag: nil, offset: 0, limit: 0, orderBy: .byDate, descending: true, in: blogID)
XCTAssertEqual(result.count, 20)
}

}

0 comments on commit 78c3311

Please sign in to comment.