Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Testable NetworkRequest #404

Merged
merged 14 commits into from
Sep 21, 2023

Conversation

timkimadobe
Copy link
Contributor

@timkimadobe timkimadobe commented Sep 20, 2023

Description

This PR:

  1. Creates the new TestableNetworkRequest that uses the @testable import AEPServices to gain open inheritable access to the NetworkRequest class.
    • TestableNetworkRequest overrides the Equatable and Hashable conformance of NetworkRequest for direct use as a dictionary key using custom logic.
    • Equatable and Hashable must have exactly the same results logically for what they consider the "same" element
  2. Uses a new defaultMockResponse in MockNetworkService that creates a default response provided a valid URL
  3. Updates NetworkServiceHelper data structs and methods to account for the new key equality behavior, and refactors usages accordingly
  4. Fixes race condition in MockNetworkService connectAsync implementation by moving the countdown to after the callback is notified, to properly gate test case logic using await

Related Issue

Motivation and Context

How Has This Been Tested?

Screenshots (if appropriate):

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist:

  • I have signed the Adobe Open Source CLA.
  • My code follows the code style of this project.
  • My change requires a change to the documentation.
  • I have updated the documentation accordingly.
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

… NetworkRequest

Override Equatable and Hashable implementations for use in testing comparisons
…orkRequest as keys

Refactor usages to use TestableNetworkRequest methods
Rename setResponseFor to addResponseFor to more accurately reflect what the method does
By doing the countdown AFTER notifying the completion handler, awaits on the expected network request can properly gate the rest of the test case logic (for example, if the test case resets the mock network service, there isn't a race condition between the reset and the get mock response, due to prematurely ungated await)
@codecov
Copy link

codecov bot commented Sep 20, 2023

Codecov Report

Merging #404 (67bebb0) into dev (c3ce8a6) will not change coverage.
The diff coverage is n/a.

@@           Coverage Diff           @@
##              dev     #404   +/-   ##
=======================================
  Coverage   96.77%   96.77%           
=======================================
  Files          27       27           
  Lines        1671     1671           
=======================================
  Hits         1617     1617           
  Misses         54       54           

// will need to be updated accordingly to handle that case.

// MARK: - Equatable (ObjC) conformance
override func isEqual(_ object: Any?) -> Bool {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment stating isEqual only checks URL scheme, host, path and http method, but not the query parameters for equality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment!

}

// MARK: - Hashable (ObjC) conformance
public override var hash: Int {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add comment stating hash only includes URL scheme, host, path and http method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment!

Comment on lines 84 to 91
func addResponseFor(networkRequest: NetworkRequest, responseConnection: HttpConnection) {
let testableNetworkRequest = TestableNetworkRequest(from: networkRequest)
if networkResponses[testableNetworkRequest] != nil {
networkResponses[testableNetworkRequest]?.append(responseConnection)
}
else {
networkResponses[testableNetworkRequest] = [responseConnection]
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should still be setResponseFor and the networkResponses type should be [NetworkRequest: HttpConnection] unless you see a need for an array of HttpConnections for a single request. If you see here in MockNetworkRequest, only the first HttpConnection is used which I believe is a side effect of not being able to group similar NetworkRequest objects together. Now with this TestableNetworkRequest, we can just store a single HttpConnection for the set of "equal" NetworkRequests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great point! Looking at the default NetworkService connectAsync implementation, I think there can only ever be a single HttpConnection response for a given request, based on the dataTask.

The only case I can think of multiple connections for the same request is maybe a recoverable failure response with retry? But even then I think we can use staggered/interleaved expectations instead of capturing both with the same key

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the following changes:

  1. Reverted the method name change from addResponseFor to setResponseFor
  2. Updated param responseConnection to be HttpConnection?
    • This has the subtle effect of removing any existing dictionary entry for the given NetworkRequest if the HttpConnection value passed is nil
  3. Updated get__ResponseFor methods to be singular instead of plural
  4. Updated integration test cases to check for single response (non-nil instead of count), and removed array access logic

@@ -58,7 +56,7 @@ class MockNetworkService: Networking {
/// Sets the mock `HttpConnection` response connection for a given `NetworkRequest`. Should only be used
/// when in mock mode.
func setMockResponseFor(networkRequest: NetworkRequest, responseConnection: HttpConnection?) {
helper.setResponseFor(networkRequest: networkRequest, responseConnection: responseConnection)
helper.addResponseFor(networkRequest: networkRequest, responseConnection: responseConnection ?? defaultMockResponse(networkRequest.url))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding a default isn't strictly needed as because of the else clause in the if let response ... statement above on line 37.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the defaults here, and updated the MockNetworkService connectAsync to own the default directly

.filter { networkRequest.isCustomEqual($0.key) }
.map { $0.value }
/// Gets all network responses for the given `NetworkRequest`
func getResponsesFor(networkRequest: NetworkRequest) -> [HttpConnection]? {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After changing networkResponses to [NetworkRequest: HttpConnection], this can return just HttpConnection? instead of an array.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated return type to HttpConnection?

Copy link
Contributor Author

@timkimadobe timkimadobe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the review @kevinlind! Updated based on feedback

Comment on lines 84 to 91
func addResponseFor(networkRequest: NetworkRequest, responseConnection: HttpConnection) {
let testableNetworkRequest = TestableNetworkRequest(from: networkRequest)
if networkResponses[testableNetworkRequest] != nil {
networkResponses[testableNetworkRequest]?.append(responseConnection)
}
else {
networkResponses[testableNetworkRequest] = [responseConnection]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a great point! Looking at the default NetworkService connectAsync implementation, I think there can only ever be a single HttpConnection response for a given request, based on the dataTask.

The only case I can think of multiple connections for the same request is maybe a recoverable failure response with retry? But even then I think we can use staggered/interleaved expectations instead of capturing both with the same key

.filter { networkRequest.isCustomEqual($0.key) }
.map { $0.value }
/// Gets all network responses for the given `NetworkRequest`
func getResponsesFor(networkRequest: NetworkRequest) -> [HttpConnection]? {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated return type to HttpConnection?

// will need to be updated accordingly to handle that case.

// MARK: - Equatable (ObjC) conformance
override func isEqual(_ object: Any?) -> Bool {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment!

}

// MARK: - Hashable (ObjC) conformance
public override var hash: Int {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added comment!

Comment on lines 84 to 91
func addResponseFor(networkRequest: NetworkRequest, responseConnection: HttpConnection) {
let testableNetworkRequest = TestableNetworkRequest(from: networkRequest)
if networkResponses[testableNetworkRequest] != nil {
networkResponses[testableNetworkRequest]?.append(responseConnection)
}
else {
networkResponses[testableNetworkRequest] = [responseConnection]
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made the following changes:

  1. Reverted the method name change from addResponseFor to setResponseFor
  2. Updated param responseConnection to be HttpConnection?
    • This has the subtle effect of removing any existing dictionary entry for the given NetworkRequest if the HttpConnection value passed is nil
  3. Updated get__ResponseFor methods to be singular instead of plural
  4. Updated integration test cases to check for single response (non-nil instead of count), and removed array access logic

@@ -58,7 +56,7 @@ class MockNetworkService: Networking {
/// Sets the mock `HttpConnection` response connection for a given `NetworkRequest`. Should only be used
/// when in mock mode.
func setMockResponseFor(networkRequest: NetworkRequest, responseConnection: HttpConnection?) {
helper.setResponseFor(networkRequest: networkRequest, responseConnection: responseConnection)
helper.addResponseFor(networkRequest: networkRequest, responseConnection: responseConnection ?? defaultMockResponse(networkRequest.url))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the defaults here, and updated the MockNetworkService connectAsync to own the default directly

Copy link
Contributor

@kevinlind kevinlind left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor comment about documentation, otherwise looks good.

return networkResponses
.filter { networkRequest.isCustomEqual($0.key) }
.map { $0.value }
/// Gets all network responses for the given `NetworkRequest`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Update comment "Get network response for the given NetworkRequest", plus add comment for parameter and return value.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated method docs!

@timkimadobe timkimadobe merged commit 285aeb9 into adobe:dev Sep 21, 2023
@timkimadobe timkimadobe deleted the testable-import-network-request branch September 29, 2023 22:42
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants