Skip to content

Commit

Permalink
feat(test): add unit tests for PlacesService and model validation
Browse files Browse the repository at this point in the history
- Implement XCTest test cases for PlacesService with mock URLSession
- Add Codable conformance tests for Place models
- Set up mock network response handling
- Fix PlacesResponse Codable implementation
  • Loading branch information
elsong86 committed Jan 30, 2025
1 parent ec4563f commit 1f70d19
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 9 deletions.
47 changes: 41 additions & 6 deletions ios/taco-about-it-ios/Model/PlaceModels.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Foundation

struct Place: Decodable, Identifiable {
// Make sure DisplayName is Codable
struct DisplayName: Codable {
let text: String
}

// Explicitly conform Place to Codable
struct Place: Codable, Identifiable {
let id: String
let displayName: DisplayName
let formattedAddress: String?
Expand All @@ -10,19 +16,48 @@ struct Place: Decodable, Identifiable {
var displayNameText: String {
displayName.text
}

// Explicitly define coding keys if needed
enum CodingKeys: String, CodingKey {
case id
case displayName
case formattedAddress
case rating
case userRatingCount
}
}

// Make PlacesRequest fully Codable
struct PlacesRequest: Codable {
let location: GeoLocation
let radius: Double
let maxResults: Int
let textQuery: String
}

struct PlacesResponse: Decodable {
// Make PlacesResponse explicitly Codable
struct PlacesResponse: Codable {
let places: [Place]
}

struct DisplayName: Decodable {
let text: String

// Explicitly define coding keys
enum CodingKeys: String, CodingKey {
case places
}

// Add explicit encode method if needed
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(places, forKey: .places)
}

// Add explicit init from decoder if needed
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
places = try container.decode([Place].self, forKey: .places)
}

// Add regular init
init(places: [Place]) {
self.places = places
}
}
11 changes: 8 additions & 3 deletions ios/taco-about-it-ios/Services/PlacesService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import Foundation
class PlacesService {
static let shared = PlacesService()
private let baseURL = "https://your-backend-url.com"
private let urlSession: URLSession

init(urlSession: URLSession = .shared) {
self.urlSession = urlSession
}

func fetchPlaces(location: GeoLocation, radius: Double = 1000.0, maxResults: Int = 20, textQuery: String = "tacos") async throws -> [Place] {
guard let url = URL(string: "\(baseURL)/places") else {
Expand All @@ -16,14 +21,14 @@ class PlacesService {
let body = PlacesRequest(location: location, radius: radius, maxResults: maxResults, textQuery: textQuery)
request.httpBody = try JSONEncoder().encode(body)

let (data, response) = try await URLSession.shared.data(for: request)
let (data, response) = try await urlSession.data(for: request)

guard let httpResponse = response as? HTTPURLResponse,
(200..<300).contains(httpResponse.statusCode) else {
throw NSError(domain: "Invalid response", code: 0, userInfo: nil)
}

let places = try JSONDecoder().decode([Place].self, from: data)
return places
let placesResponse = try JSONDecoder().decode(PlacesResponse.self, from: data)
return placesResponse.places
}
}
57 changes: 57 additions & 0 deletions ios/taco-about-it-iosTests/ModelTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import XCTest
@testable import taco_about_it_ios

final class ModelTests: XCTestCase {

func testDisplayNameCodable() throws {
// Given
let original = DisplayName(text: "Test Place")

// When
let encoded = try JSONEncoder().encode(original)
let decoded = try JSONDecoder().decode(DisplayName.self, from: encoded)

// Then
XCTAssertEqual(original.text, decoded.text)
}

func testPlaceCodable() throws {
// Given
let original = Place(
id: "test-id",
displayName: DisplayName(text: "Test Place"),
formattedAddress: "123 Test St",
rating: 4.5,
userRatingCount: 100
)

// When
let encoded = try JSONEncoder().encode(original)
let decoded = try JSONDecoder().decode(Place.self, from: encoded)

// Then
XCTAssertEqual(original.id, decoded.id)
XCTAssertEqual(original.displayNameText, decoded.displayNameText)
}

func testPlacesResponseCodable() throws {
// Given
let place = Place(
id: "test-id",
displayName: DisplayName(text: "Test Place"),
formattedAddress: "123 Test St",
rating: 4.5,
userRatingCount: 100
)
let original = PlacesResponse(places: [place])

// When
let encoded = try JSONEncoder().encode(original)
print(String(data: encoded, encoding: .utf8) ?? "") // For debugging
let decoded = try JSONDecoder().decode(PlacesResponse.self, from: encoded)

// Then
XCTAssertEqual(original.places.count, decoded.places.count)
XCTAssertEqual(original.places[0].id, decoded.places[0].id)
}
}
94 changes: 94 additions & 0 deletions ios/taco-about-it-iosTests/PlacesServiceTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import XCTest
@testable import taco_about_it_ios

class URLProtocolMock: URLProtocol {
static var requestHandler: ((URLRequest) throws -> (HTTPURLResponse, Data))?

override class func canInit(with request: URLRequest) -> Bool {
return true
}

override class func canonicalRequest(for request: URLRequest) -> URLRequest {
return request
}

override func startLoading() {
guard let handler = URLProtocolMock.requestHandler else {
XCTFail("Handler is unavailable.")
return
}

do {
let (response, data) = try handler(request)
client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed)
client?.urlProtocol(self, didLoad: data)
client?.urlProtocolDidFinishLoading(self)
} catch {
client?.urlProtocol(self, didFailWithError: error)
}
}

override func stopLoading() {}
}

final class PlacesServiceTests: XCTestCase {
var sut: PlacesService!
var mockURLSession: URLSession!

override func setUp() {
super.setUp()

// Configure mock URLSession
let configuration = URLSessionConfiguration.ephemeral
configuration.protocolClasses = [URLProtocolMock.self]
mockURLSession = URLSession(configuration: configuration)

sut = PlacesService(urlSession: mockURLSession)
}

override func tearDown() {
sut = nil
mockURLSession = nil
URLProtocolMock.requestHandler = nil
super.tearDown()
}

func testFetchPlacesWithValidLocation() async throws {
// Given
let location = GeoLocation(latitude: 37.7749, longitude: -122.4194)

// Create a PlacesResponse object
let mockPlacesResponse = PlacesResponse(places: [
Place(
id: "test-id",
displayName: DisplayName(text: "Test Taco Place"),
formattedAddress: "123 Test St",
rating: 4.5,
userRatingCount: 100
)
])

// Encode the response
let mockData = try JSONEncoder().encode(mockPlacesResponse)

let mockHTTPResponse = HTTPURLResponse(
url: URL(string: "https://your-backend-url.com/places")!,
statusCode: 200,
httpVersion: nil,
headerFields: ["Content-Type": "application/json"]
)!

// Set up the mock handler
URLProtocolMock.requestHandler = { request in
return (mockHTTPResponse, mockData)
}

// When
let places = try await sut.fetchPlaces(location: location)

// Then
XCTAssertEqual(places.count, 1)
XCTAssertEqual(places[0].id, "test-id")
XCTAssertEqual(places[0].displayNameText, "Test Taco Place")
}
}

0 comments on commit 1f70d19

Please sign in to comment.