[TDD] URLSession — Integration Test

Make sure all components work together

Muhammad Alfiansyah
3 min readNov 25, 2024
https://marsner.com/blog/why-test-driven-development-tdd/

After we test our unit or component we will going deeper by Testing our Integration and make sure all components work together.

In this article we will test our implementation before (HTTPClient, HTTPClientErrorMapperDecorator and ModelMapperAdapter).

First we will test HTTPClient and make sure they return correct response. In this step we wont use Mock, Spy or Stub.

We will use Rick and Morty API for simplify our test.

final class HTTPClientIntegrationTests: XCTestCase {
func test_loadCharacterEndpoint_returnsValidResponse() async throws {
// Arrange: Set up the base URL and request
let baseUrl = URL(string: "https://rickandmortyapi.com/api/")!
let urlRequest = URLRequestBuilder(path: "character")
.method(.get)
.makeRequest(withBaseURL: baseUrl)

let sut = HTTPClient()

// Act: Perform the network request
let (data, response) = try await sut.load(urlRequest: urlRequest)

// Assert: Validate the response
guard let httpResponse = response as? HTTPURLResponse else {
XCTFail("Response is not an HTTPURLResponse")
return
}

XCTAssertEqual(httpResponse.statusCode, 200, "Expected a 200 OK status code")

let jsonObject = try JSONSerialization.jsonObject(with: data)
XCTAssertNotNil(jsonObject, "Expected non-nil JSON object")

// Optional: Validate JSON formatting (useful for debugging)
let object = try JSONSerialization.jsonObject(with: data)
let prettyPrintedData = try JSONSerialization.data(
withJSONObject: object,
options: [.prettyPrinted, .sortedKeys]
)
let prettyPrintedString = String(data: prettyPrintedData, encoding: .utf8)
XCTAssertNotNil(prettyPrintedString)
}
}

We assert response status code then make sure we can serialize Data as JSON string, so we confidence that code return correct JSON format.

Then we can test Error Mapper that we create before

final class HTTPClientIntegrationTests: XCTestCase {
func test_loadCharactherEndpoint_returnsErrorAfterMapping() async throws {
let baseUrl = URL(string: "https://rickandmortyapi.com/api/")!
let urlRequest = URLRequestBuilder(path: "character/827")
.method(.get)
.makeRequest(withBaseURL: baseUrl)

let client = HTTPClient()
let sut = HTTPClientErrorMapperDecorator(decoratee: client)

do {
_ = try await sut.load(urlRequest: urlRequest)
XCTFail()
} catch {
XCTAssertTrue(error is HTTPClientError)
XCTAssertEqual(error as? HTTPClientError, .notFoundError)
}
}
}

Next we will test our Model Mapper, before we create test we will create our Decodable Model, based on the page result contain info and results properties like bellow

struct ListResponse: Decodable {
let info: InfoResponse
let results: [Character]
}

struct InfoResponse: Decodable {
let count: Int
let pages: Int
let next: String?
let pre: String?
}

struct Character: Decodable {
let id: Int
let name: String
let status: String
let species: String
let gender: String
let image: String
}

The we will create the test like this

final class HTTPClientIntegrationTests: XCTestCase {
func test_loadCharactherEndpoint_returnsCharacter() async throws {
let baseUrl = URL(string: "https://rickandmortyapi.com/api/")!
let urlRequest = URLRequestBuilder(path: "character")
.method(.get)
.makeRequest(withBaseURL: baseUrl)

let client = HTTPClient()
let sut = ModelMapperAdapter(client: client)

let result = try await sut.load(urlRequest: urlRequest, model: ListResponse.self)
XCTAssertEqual(result.info.count, 826)
XCTAssertEqual(result.results.count, 20)
}
}

Just run the test and check if test is passes ?

And its passes, so we are DONE on the integration test. What about next ?

You can move your code on the production folder and expose protocol or class to the public if you want to share to other module.

--

--

No responses yet