From 42be7503f4a5f7072e47907cf8c90ceefb4d0919 Mon Sep 17 00:00:00 2001 From: Thomas De Leon <3507743+tdeleon@users.noreply.github.com> Date: Mon, 29 Jan 2024 15:30:27 -0800 Subject: [PATCH] #29: Add tests for json decoder inheritance; update documentation --- README.md | 8 +-- .../SendingRequests/SendingRequestsHandler.md | 4 +- .../SendingRequestsPublisher.md | 4 +- Sources/Relax/Request/Request+Send.swift | 41 ++++++++++++++ .../AsyncTests/AsyncRequestTests.swift | 40 ++++++++++++++ .../CombineTests/CombineCodableTests.swift | 41 +++++++++++++- .../CombineTests/CombineRequestTests.swift | 29 ++++++++++ .../CompletionRequestTests.swift | 53 +++++++++++++++++++ Tests/RelaxTests/Helpers/MockServices.swift | 28 ++++++++++ Tests/RelaxTests/Request/RequestTests.swift | 10 ++++ Tests/RelaxTests/Request/ServiceTests.swift | 37 ------------- 11 files changed, 249 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index 78d3eb8..2933ea2 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Relax supports the [Swift Package Manager](https://www.swift.org/package-manager 4. Click on **Add Package** >Tip: `URLMock` is an additional framework provided to aid in testing by mocking responses to requests, and should be -added to your test targets only. For more information, see below. +added to your test targets only. For more information, see [Testing](#testing) below. #### Import the framework @@ -114,7 +114,8 @@ let request = Request(.post, url: URL(string: "https://example.com/users")!) { } ``` -See for more details. +See [Defining Requests](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingrequests) for more +details. ### Define a Complex API Structure @@ -147,7 +148,8 @@ enum UserService: Service { let users = try await UserService.Users.getAll.send() ``` -See for more details. +See [Defining an APIStructure](https://swiftpackageindex.com/tdeleon/relax/documentation/relax/definingapistructure) for +more details. ## Testing diff --git a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md index 84ecf0e..718dac4 100644 --- a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md +++ b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsHandler.md @@ -82,12 +82,12 @@ returns the data decoded to a `Decodable` type. ```swift let request = Request(.get, url: URL(string: "https://example.com")!) -request.send { (result: Result in +request.send { (result: Result) in switch result { case .success(let user): print("User: \(user)") case .failure(let error): - print("Request failed - \(error.localizedDescription)") + print("Request failed - \(error)") } } ``` diff --git a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md index 8d05f98..9ba2489 100644 --- a/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md +++ b/Sources/Relax/Relax.docc/Request/SendingRequests/SendingRequestsPublisher.md @@ -24,7 +24,7 @@ cancellable = request.send() .sink(receiveCompletion: { completion in switch completion { case .failure(let error): - print("Request failed - \(error.localizedDescription)") + print("Request failed - \(error)") case .finished: break } @@ -109,7 +109,7 @@ cancellable = try request.send() case .finished: break } - }, receiveValue: { response in + }, receiveValue: { (response: User) in print("User: \(response.responseModel)") }) ``` diff --git a/Sources/Relax/Request/Request+Send.swift b/Sources/Relax/Request/Request+Send.swift index 7bba456..74c4aa4 100644 --- a/Sources/Relax/Request/Request+Send.swift +++ b/Sources/Relax/Request/Request+Send.swift @@ -84,11 +84,52 @@ extension Request { return task } + /// Send a request with a completion handler, decoding the received data to a Decodable instance + /// - Parameters: + /// - decoder: When set, overrides the ``Request/decoder`` used to decode received data. + /// - session: When set, overrides the ``Request/session`` used to send the request. + /// - completion: A completion handler with a `Result` of the model type decoded from received data, or ``RequestError`` on failure. + /// + /// + /// Use this method when you want to decode data into a given model type, and do not need the full `HTTPURLResponse` from the server. + /// + /// ```swift + /// let request = Request(.get, url: URL(string: "https://example.com")!) + /// request.send { (result: Result) in + /// switch result { + /// case .success(let user): + /// print("User: \(user)") + /// case .failure(let error): + /// print("Request failed - \(error)") + /// } + /// } + /// ``` + /// + public func send( + decoder: JSONDecoder? = nil, + session: URLSession? = nil, + completion: @escaping (_ result: Result) -> Void + ) { + send( + decoder: decoder, + session: session + ) { (result: Result, RequestError>) in + switch result { + case .success(let success): + completion(.success(success.responseModel)) + case .failure(let failure): + completion(.failure(failure)) + } + } + } + /// Send a request with a completion handler, decoding the received data to a Decodable instance /// - Parameters: /// - decoder: When set, overrides the ``Request/decoder`` used to decode received data. /// - session: When set, overrides the ``Request/session`` used to send the request. /// - completion: A completion handler with the response from the server, including the decoded data as the Decodable type. + /// + /// Use this method when decoding a model `Decodable` type and you also need the full `HTTPURLResponse` from the server. public func send( decoder: JSONDecoder? = nil, session: URLSession? = nil, diff --git a/Tests/RelaxTests/AsyncTests/AsyncRequestTests.swift b/Tests/RelaxTests/AsyncTests/AsyncRequestTests.swift index af7c663..fd20f27 100644 --- a/Tests/RelaxTests/AsyncTests/AsyncRequestTests.swift +++ b/Tests/RelaxTests/AsyncTests/AsyncRequestTests.swift @@ -57,5 +57,45 @@ final class AsyncRequestTests: XCTestCase { func testComplexRequest() async throws { try await makeSuccess(request: service.ComplexRequests.complex) } + + func testOverrideSession() async throws { + let expectation = self.expectation(description: "Mock received") + let expectedSession = URLMock.session(.mock { _ in + expectation.fulfill() + }) + + let override = Request(.get, parent: InheritService.User.self, session: expectedSession) + try await override.send() + + await fulfillment(of: [expectation], timeout: 1) + } + + func testOverrideSessionOnSendAsync() async throws { + let expectation = self.expectation(description: "Mock received") + let expectedSession = URLMock.session(.mock { _ in + expectation.fulfill() + }) + try await InheritService.User.get.send(session: expectedSession) + + await fulfillment(of: [expectation], timeout: 1) + } + + func testOverrideDecoderOnSend() async throws { + let model = InheritService.User.Response(date: .now) + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + let session = URLMock.session(.mock(model, encoder: encoder)) + + // send request - the defined JSONDecoder on InheritService has 8601 date decoding strategy + let _: InheritService.User.Response = try await InheritService.User.get.send(session: session) + + do { + // send request, overriding the inherited decoder with default - should fail to parse + let _: InheritService.User.Response = try await InheritService.User.get.send(decoder: JSONDecoder(), session: session) + XCTFail("Should have failed") + } catch { + XCTAssertTrue(error is RequestError) + } + } } #endif diff --git a/Tests/RelaxTests/CombineTests/CombineCodableTests.swift b/Tests/RelaxTests/CombineTests/CombineCodableTests.swift index d4ee68e..e5c27bd 100644 --- a/Tests/RelaxTests/CombineTests/CombineCodableTests.swift +++ b/Tests/RelaxTests/CombineTests/CombineCodableTests.swift @@ -15,7 +15,7 @@ import URLMock final class CombineCodableTests: XCTestCase { typealias User = ExampleService.Users.User var session: URLSession! - var cancellable: AnyCancellable? + var cancellables = Set() let service = ExampleService.Users.self @@ -32,7 +32,7 @@ final class CombineCodableTests: XCTestCase { URLMock.response = .mock(sampleModel) let expectation = self.expectation(description: "Expect") - cancellable = service.getRequest + service.getRequest .send(session: session) .sink(receiveCompletion: { completion in defer { expectation.fulfill() } @@ -45,8 +45,45 @@ final class CombineCodableTests: XCTestCase { }, receiveValue: { (response: [User]) in XCTAssertEqual(response, sampleModel) }) + .store(in: &cancellables) + waitForExpectations(timeout: 1) } + + func testOverrideDecoderOnSend() throws { + let success = self.expectation(description: "Success") + let failure = self.expectation(description: "Failure") + let model = InheritService.User.Response(date: .now) + let session = URLMock.session(.mock(model, encoder: InheritService.iso8601Encoder)) + + InheritService.User.get.send(session: session) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure: + XCTFail("Should succeed") + } + }, receiveValue: { (response: InheritService.User.Response) in + success.fulfill() + }) + .store(in: &cancellables) + + InheritService.User.get.send(decoder: JSONDecoder(), session: session) + .sink(receiveCompletion: { completion in + switch completion { + case .finished: + break + case .failure: + failure.fulfill() + } + }, receiveValue: { (response: InheritService.User.Response) in + XCTFail() + }) + .store(in: &cancellables) + + waitForExpectations(timeout: 1) + } } #endif diff --git a/Tests/RelaxTests/CombineTests/CombineRequestTests.swift b/Tests/RelaxTests/CombineTests/CombineRequestTests.swift index 4b15b71..6b11a87 100644 --- a/Tests/RelaxTests/CombineTests/CombineRequestTests.swift +++ b/Tests/RelaxTests/CombineTests/CombineRequestTests.swift @@ -69,6 +69,35 @@ final class CombineRequestTests: XCTestCase { makeSuccess(request: ExampleService.ComplexRequests.complex) } + func testOverrideSession() throws { + let expectation = self.expectation(description: "Mock received") + let session = URLMock.session(.mock { _ in + expectation.fulfill() + }) + + let override = Request(.get, parent: InheritService.User.self, session: session) + cancellable = override.send() + .sink(receiveCompletion: { _ in + }, receiveValue: { _ in + }) + + waitForExpectations(timeout: 1) + } + + func testOverrideSessionOnSend() throws { + let expectation = self.expectation(description: "Mock received") + let session = URLMock.session(.mock { _ in + expectation.fulfill() + }) + + cancellable = InheritService.User.get.send(session: session) + .sink(receiveCompletion: { _ in + }, receiveValue: { _ in + }) + + waitForExpectations(timeout: 1) + } + func testGetPerformance() throws { measure { makeSuccess(request: ExampleService.get) diff --git a/Tests/RelaxTests/CompletionTests/CompletionRequestTests.swift b/Tests/RelaxTests/CompletionTests/CompletionRequestTests.swift index 2bcc3cc..dedfa5b 100644 --- a/Tests/RelaxTests/CompletionTests/CompletionRequestTests.swift +++ b/Tests/RelaxTests/CompletionTests/CompletionRequestTests.swift @@ -65,6 +65,59 @@ final class CompletionRequestTests: XCTestCase { makeSuccess(request: ExampleService.ComplexRequests.complex) } + func testOverrideDecoderOnSend() throws { + let success = self.expectation(description: "success") + let fail = self.expectation(description: "fail") + let model = InheritService.User.Response(date: .now) + + let session = URLMock.session(.mock(model, encoder: InheritService.iso8601Encoder)) + + InheritService.User.get.send(session: session) { (result: Result) in + switch result { + case .success: + success.fulfill() + case .failure: + XCTFail("Failed") + } + } + + InheritService.User.get.send(decoder: JSONDecoder(), session: session) { (result: Result) in + switch result { + case .success: + XCTFail("Should fail") + case .failure: + fail.fulfill() + } + } + + waitForExpectations(timeout: 1) + } + + func testOverrideSession() throws { + let expectation = self.expectation(description: "Mock received") + let expectedSession = URLMock.session(.mock { _ in + expectation.fulfill() + }) + + let override = Request(.get, parent: InheritService.User.self, session: expectedSession) + override.send(session: expectedSession) { _ in + } + + waitForExpectations(timeout: 1) + } + + func testOverrideSessionOnSend() throws { + let expectation = self.expectation(description: "Mock received") + let expectedSession = URLMock.session(.mock { _ in + expectation.fulfill() + }) + + InheritService.User.get.send(session: expectedSession) { _ in + } + + waitForExpectations(timeout: 1) + } + func testGetPerformance() throws { measure { makeSuccess(request: ExampleService.get) diff --git a/Tests/RelaxTests/Helpers/MockServices.swift b/Tests/RelaxTests/Helpers/MockServices.swift index 174c7b0..eedb084 100644 --- a/Tests/RelaxTests/Helpers/MockServices.swift +++ b/Tests/RelaxTests/Helpers/MockServices.swift @@ -153,3 +153,31 @@ struct BadURLService: Service { } } } + +enum InheritService: Service { + static let baseURL = URL(string: "https://example.com")! + static var configuration: Request.Configuration = Request.Configuration(allowsCellularAccess: false) + static let session: URLSession = URLSession(configuration: .ephemeral) + static let decoder: JSONDecoder = { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601 + return decoder + }() + + static let iso8601Encoder: JSONEncoder = { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + return encoder + }() + + enum User: Endpoint { + static var path: String = "users" + typealias Parent = InheritService + + static let get = Request(.get, parent: User.self) + + struct Response: Codable, Hashable { + var date: Date + } + } +} diff --git a/Tests/RelaxTests/Request/RequestTests.swift b/Tests/RelaxTests/Request/RequestTests.swift index a69c2d9..c9e7232 100644 --- a/Tests/RelaxTests/Request/RequestTests.swift +++ b/Tests/RelaxTests/Request/RequestTests.swift @@ -142,4 +142,14 @@ final class RequestTests: XCTestCase { XCTAssertEqual(request._properties, properties) } + func testHashable() { + let request1 = Request(.get, url: URL(string: "https://example.com/")!) + let request2 = request1 + let request3 = Request(.delete, url: URL(string: "https://example.com/")!) + let request4 = Request(.get, url: URL(string: "https://example.org/")!) + + XCTAssertEqual(request1.hashValue, request2.hashValue) + XCTAssertNotEqual(request1.hashValue, request3.hashValue) + XCTAssertNotEqual(request1.hashValue, request4.hashValue) + } } diff --git a/Tests/RelaxTests/Request/ServiceTests.swift b/Tests/RelaxTests/Request/ServiceTests.swift index 52eb41a..4e3d6ee 100644 --- a/Tests/RelaxTests/Request/ServiceTests.swift +++ b/Tests/RelaxTests/Request/ServiceTests.swift @@ -15,19 +15,6 @@ import URLMock final class ServiceTests: XCTestCase { typealias Complex = ExampleService.ComplexRequests typealias SubComplex = ExampleService.ComplexRequests.SubComplex - - enum InheritService: Service { - static let baseURL = URL(string: "https://example.com")! - static var configuration: Request.Configuration = Request.Configuration(allowsCellularAccess: false) - static let session: URLSession = URLSession(configuration: .ephemeral) - - enum User: Endpoint { - static var path: String = "users" - typealias Parent = InheritService - - static let get = Request(.get, parent: User.self) - } - } func testBasic() { let basic = ExampleService.get @@ -85,30 +72,6 @@ final class ServiceTests: XCTestCase { let override = Request(.get, parent: InheritService.User.self, configuration: expectedConfiguration) XCTAssertEqual(override.configuration, expectedConfiguration) } - -#if swift(>=5.9) - func testOverrideSession() async throws { - let expectation = self.expectation(description: "Mock received") - let expectedSession = URLMock.session(.mock { _ in - expectation.fulfill() - }) - - let override = Request(.get, parent: InheritService.User.self, session: expectedSession) - try await override.send() - - await fulfillment(of: [expectation], timeout: 1) - } - - func testOverrideSessionOnSend() async throws { - let expectation = self.expectation(description: "Mock received") - let expectedSession = URLMock.session(.mock { _ in - expectation.fulfill() - }) - try await InheritService.User.get.send(session: expectedSession) - - await fulfillment(of: [expectation], timeout: 1) - } -#endif } extension URL {