Skip to content

Commit

Permalink
Release 0.7.0 - JSON injection added for testing
Browse files Browse the repository at this point in the history
  • Loading branch information
Maciej Burdzicki committed Sep 25, 2024
1 parent 319ec42 commit 45c3b0d
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 17 deletions.
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Meet **Snowdrop** - type-safe, easy to use framework powered by Swift Macros cre
- [Arguments' Default Values](#arguments-default-values)
- [Interceptors](#interceptors)
- [Mockable](#mockable)
- [JSON Injection](#json-injection)
- [Acknowledgements](#acknowledgements)

## Installation
Expand Down Expand Up @@ -206,12 +207,27 @@ Snowdrop will automatically create a `EndpointServiceMock` class with all the pr

```Swift
func testEmptyArrayResult() async throws {
let mock = EndpointServiceMock(baseUrl: URL(string: "https://some.url")!
mock.getPostsResult = .success([])
let mock = EndpointServiceMock(baseUrl: URL(string: "https://some.url")!
mock.getPostsResult = .success([])

let result = try await mock.getPosts()
let result = try await mock.getPosts()
XCTAssertTrue(result.isEmpty)
}
```

### JSON Injection

If you'd like to test your service against mocked JSONs, you can easily do it. Just make sure you got your JSON mock somewhere in your project files, then instantiate your service with `testMode` flag set to `true` and determine for which request your mock should be injected like in the example below.

XCTAssertTrue(result.isEmpty)
```Swift
func testJSONMockInjectsion() async throws {
let service = MyEndpointService(baseUrl: someBaseURL, testMode: true)
service.testJSONDictionary = ["users/123/info": "MyJSONMock"]

let result = try await service.getUserInfo(id: 123)
XCTAssertTrue(result.firstName, "JSON")
XCTAssertTrue(result.lastName, "Bourne")
}
```

**Note that mocked methods will directly return stubbed result without accessing Snowdrop.Core so your beforeSend and onResponse blocks won't be called.**
Expand Down
41 changes: 37 additions & 4 deletions Sources/Snowdrop/Core/SnowdropCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,12 @@ public extension Snowdrop.Core {
@discardableResult
func performRequest(
_ request: URLRequest,
baseUrl: URL,
pinning: PinningMode?,
urlsExcludedFromPinning: [String],
requestBlocks: [String: RequestHandler],
responseBlocks: [String: ResponseHandler]
responseBlocks: [String: ResponseHandler],
testJSONDictionary: [String: String]?
) async throws -> (Data?, HTTPURLResponse) {
let session = getSession(pinningMode: pinning, urlsExcludedFromPinning: urlsExcludedFromPinning)
var data: Data?
Expand All @@ -26,7 +28,7 @@ public extension Snowdrop.Core {
applyRequestBlocks(requestBlocks, for: &finalRequest)

do {
(data, urlResponse) = try await session.data(for: finalRequest)
(data, urlResponse) = try await executeRequest(baseUrl: baseUrl, session: session, request: finalRequest, testJSONDictionary: testJSONDictionary)
session.finishTasksAndInvalidate()
} catch {
throw handleError(error as NSError)
Expand All @@ -42,18 +44,22 @@ public extension Snowdrop.Core {

func performRequestAndDecode<T: Codable>(
_ request: URLRequest,
baseUrl: URL,
decoder: JSONDecoder,
pinning: PinningMode?,
urlsExcludedFromPinning: [String],
requestBlocks: [String: RequestHandler],
responseBlocks: [String: ResponseHandler]
responseBlocks: [String: ResponseHandler],
testJSONDictionary: [String: String]?
) async throws -> T {
let (data, _) = try await performRequest(
request,
baseUrl: baseUrl,
pinning: pinning,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks
responseBlocks: responseBlocks,
testJSONDictionary: testJSONDictionary
)

guard let unwrappedData = data else { throw SnowdropError(type: .unexpectedResponse) }
Expand All @@ -74,6 +80,33 @@ public extension Snowdrop.Core {
}
}

func executeRequest(baseUrl: URL, session: URLSession, request: URLRequest, testJSONDictionary: [String: String]?) async throws -> (Data?, URLResponse?) {
var data: Data?
var urlResponse: URLResponse?
let jsonPaths = [".json", ".JSON"].reduce([]) { $0 + Bundle.main.paths(forResourcesOfType: $1, inDirectory: nil) }

if let testJSONDictionary,
let requestUrl = request.url,
let key = testJSONDictionary.keys.first(where: { baseUrl.appendingPathComponent($0).absoluteString == requestUrl.absoluteString }),
let jsonName = testJSONDictionary[key],
let jsonPath = jsonPaths.first(where: { $0.hasSuffix(jsonName + ".json") || $0.hasSuffix(jsonName + ".JSON") }),
let jsonData = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)) {

data = jsonData
urlResponse = HTTPURLResponse(url: requestUrl, statusCode: 200, httpVersion: nil, headerFields: nil)
return (data, urlResponse)
}

do {
(data, urlResponse) = try await session.data(for: request)
session.finishTasksAndInvalidate()
} catch {
throw handleError(error as NSError)
}

return (data, urlResponse)
}

private func handleNon200Code(from response: HTTPURLResponse, data: Data?) throws {
guard !(200..<300 ~= response.statusCode) else { return }
throw generateError(from: response, data: data)
Expand Down
5 changes: 4 additions & 1 deletion Sources/Snowdrop/Service.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ public protocol Service {
var requestBlocks: [String: RequestHandler] { get set }
var responseBlocks: [String: ResponseHandler] { get set }

var testJSONDictionary: [String: String]? { get set }

var decoder: JSONDecoder { get set }
var pinningMode: PinningMode? { get set }
var urlsExcludedFromPinning: [String] { get set }

init(baseUrl: URL,
pinningMode: PinningMode?,
urlsExcludedFromPinning: [String],
decoder: JSONDecoder
decoder: JSONDecoder,
testMode: Bool
)

func addBeforeSendingBlock(for path: String?, _ block: @escaping RequestHandler)
Expand Down
7 changes: 6 additions & 1 deletion Sources/SnowdropMacros/ClassBuilder/ClassBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,25 @@ struct ClassBuilder {
\(raw: accessModifier)var requestBlocks: [String: RequestHandler] = [:]
\(raw: accessModifier)var responseBlocks: [String: ResponseHandler] = [:]
\(raw: accessModifier)var testJSONDictionary: [String: String]?
\(raw: accessModifier)var decoder: JSONDecoder
\(raw: accessModifier)var pinningMode: PinningMode?
\(raw: accessModifier)var urlsExcludedFromPinning: [String]
private let testMode: Bool
\(raw: accessModifier)required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init()
decoder: JSONDecoder = .init(),
testMode: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
}
\(raw: ClassBuilder.buildBeforeSendingBlockFunc(for: type, accessModifier: accessModifier))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,25 @@ struct ServiceRequestBuilder: ClassMethodBodyBuilderProtocol {
return try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
"""
} else {
requestImpl += """
_ = try\(details.doesThrow ? "" : "?") await Snowdrop.core.performRequest(
request,
baseUrl: baseUrl,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
"""
}
Expand Down
29 changes: 24 additions & 5 deletions Tests/SnowdropMacrosTests/SnowdropMacrosTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,25 @@ final class SnowdropMacrosTests: XCTestCase {
var requestBlocks: [String: RequestHandler] = [:]
var responseBlocks: [String: ResponseHandler] = [:]
var testJSONDictionary: [String: String]?
var decoder: JSONDecoder
var pinningMode: PinningMode?
var urlsExcludedFromPinning: [String]
private let testMode: Bool
required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init()
decoder: JSONDecoder = .init(),
testMode: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
}
func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand Down Expand Up @@ -97,11 +102,13 @@ final class SnowdropMacrosTests: XCTestCase {
return try await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
}
Expand Down Expand Up @@ -160,20 +167,25 @@ final class SnowdropMacrosTests: XCTestCase {
public var requestBlocks: [String: RequestHandler] = [:]
public var responseBlocks: [String: ResponseHandler] = [:]
public var testJSONDictionary: [String: String]?
public var decoder: JSONDecoder
public var pinningMode: PinningMode?
public var urlsExcludedFromPinning: [String]
private let testMode: Bool
public required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init()
decoder: JSONDecoder = .init(),
testMode: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
}
public func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand Down Expand Up @@ -222,11 +234,13 @@ final class SnowdropMacrosTests: XCTestCase {
return try await Snowdrop.core.performRequestAndDecode(
request,
baseUrl: baseUrl,
decoder: decoder,
pinning: pinningMode,
urlsExcludedFromPinning: urlsExcludedFromPinning,
requestBlocks: requestBlocks,
responseBlocks: responseBlocks
responseBlocks: responseBlocks,
testJSONDictionary: testMode ? testJSONDictionary : nil
)
}
Expand Down Expand Up @@ -285,20 +299,25 @@ final class SnowdropMacrosTests: XCTestCase {
public var requestBlocks: [String: RequestHandler] = [:]
public var responseBlocks: [String: ResponseHandler] = [:]
public var testJSONDictionary: [String: String]?
public var decoder: JSONDecoder
public var pinningMode: PinningMode?
public var urlsExcludedFromPinning: [String]
private let testMode: Bool
public required init(
baseUrl: URL,
pinningMode: PinningMode? = nil,
urlsExcludedFromPinning: [String] = [],
decoder: JSONDecoder = .init()
decoder: JSONDecoder = .init(),
testMode: Bool = false
) {
self.baseUrl = baseUrl
self.pinningMode = pinningMode
self.urlsExcludedFromPinning = urlsExcludedFromPinning
self.decoder = decoder
self.testMode = testMode
}
public func addBeforeSendingBlock(for path: String? = nil, _ block: @escaping RequestHandler) {
Expand Down

0 comments on commit 45c3b0d

Please sign in to comment.