diff --git a/mobile-client/ToDoListTests/AddToDoReducerTests.swift b/mobile-client/ToDoListTests/AddToDoReducerTests.swift index b53df72..9099119 100644 --- a/mobile-client/ToDoListTests/AddToDoReducerTests.swift +++ b/mobile-client/ToDoListTests/AddToDoReducerTests.swift @@ -6,18 +6,21 @@ import Testing @testable import ToDoList struct AddToDoReducerTests { + /// Tests the successful saving of a new ToDo item. @Test func testSaveToDoSuccess() async { + // Arrange: Create a new ToDo item and a mock service. let newTodo = ToDoItem(id: 0, title: "New Task", deadline: nil, status: "pending", tags: [], createdAt: "", updatedAt: "") - let mockService = MockToDoRemoteService() + // Create a test store with an initial state and dependencies. let store = await TestStore(initialState: AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: ""))) { AddToDoReducer() } withDependencies: { $0.toDoService = mockService } + // Act: Set the title and status of the ToDo item. await store.send(.setTitle("New Task")) { $0.todo.title = "New Task" } @@ -25,27 +28,32 @@ struct AddToDoReducerTests { $0.todo.status = "pending" } + // Trigger the save action. await store.send(.saveButtonTapped) { $0.isSaving = true } + // Assert: Receive the success response and check the state. await store.receive(.saveResponse(.success(newTodo))) { $0.isSaving = false } } + /// Tests the failure case when saving a new ToDo item. @Test func testSaveToDoFailure() async { - // Set up mock remote service + // Arrange: Set up a mock remote service that simulates an error. let mockService = MockToDoRemoteService() mockService.error = NSError(domain: "Fail", code: 0) + // Create a test store with an initial state and dependencies. let store = await TestStore(initialState: AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: ""))) { AddToDoReducer() } withDependencies: { $0.toDoService = mockService } + // Act: Set the title and status of the ToDo item. await store.send(.setTitle("New Task")) { $0.todo.title = "New Task" } @@ -53,10 +61,12 @@ struct AddToDoReducerTests { $0.todo.status = "pending" } + // Trigger the save action. await store.send(.saveButtonTapped) { $0.isSaving = true } + // Assert: Receive the failure response and check the state for the error message. await store.receive(.saveResponse(.failure(.networkError(mockService.error!)))) { $0.isSaving = false $0.saveError = "Network error: The operation couldn’t be completed. (Fail error 0.)" diff --git a/mobile-client/ToDoListTests/ToDoLocalServiceTests.swift b/mobile-client/ToDoListTests/ToDoLocalServiceTests.swift index bab4da9..2378208 100644 --- a/mobile-client/ToDoListTests/ToDoLocalServiceTests.swift +++ b/mobile-client/ToDoListTests/ToDoLocalServiceTests.swift @@ -6,91 +6,100 @@ struct ToDoLocalServiceTests { private var context: ModelContext private var service: ToDoLocalService + /// Initializes a new instance of `ToDoLocalServiceTests`. init() { + // Configure the model to be stored in memory only for testing purposes. let config = ModelConfiguration(isStoredInMemoryOnly: true) let container = try! ModelContainer(for: ToDoItemData.self, configurations: config) context = ModelContext(container) service = ToDoLocalService(context: context) } + /// Tests saving and fetching ToDo items. @Test func testSaveAndFetchTodos() { - let todo = - ToDoItemData(id: 0, - title: "Grocery Shopping", - deadline: nil, - status: "todo", - tags: [Tag(name: "groceries"), Tag(name: "shopping")], - createdAt: "now", - updatedAt: "now") + // Arrange: Create a new ToDo item to save. + let todo = ToDoItemData(id: 0, + title: "Grocery Shopping", + deadline: nil, + status: "todo", + tags: [Tag(name: "groceries"), Tag(name: "shopping")], + createdAt: "now", + updatedAt: "now") try! service.save(todo: todo) + // Act: Fetch the saved ToDo items. let todos = try! service.fetchTodos() + + // Assert: Check that the count and the title match expectations. #expect(todos.count == 1) let fetchedTodo = todos.first #expect(fetchedTodo?.title == "Grocery Shopping") } + /// Tests saving multiple ToDo items and their fetch order. @Test func testSaveAndFetchTodosFetchOrder() { - let todo1 = - ToDoItemData(id: 0, - title: "Prepare Presentation", - deadline: "2023-05-13T20:49:00.000Z", - status: "todo", - tags: [], - createdAt: "2023-12-01T12:22:30.356Z", - updatedAt: "2023-12-08T12:22:30.356Z") - let todo2 = - ToDoItemData(id: 1, - title: "Visit Doctor", - deadline: "2023-05-13T20:49:00.000+08:00", - status: "todo", - tags: [Tag(name: "health")], - createdAt: "2023-12-02T12:22:30.356Z", - updatedAt: "2023-12-10T12:22:30.356Z") - let todo3 = - ToDoItemData(id: 2, - title: "Finish Book", - deadline: "2024-12-30T15:12:26.676Z", - status: "todo", - tags: [Tag(name: "reading")], - createdAt: "2023-12-03T12:22:30.356Z", - updatedAt: "2023-12-09T12:22:30.356Z") - - let todo4 = - ToDoItemData(id: 3, - title: "Finish Book", - deadline: nil, - status: "todo", - tags: [Tag(name: "reading")], - createdAt: "2023-12-03T12:22:30.356Z", - updatedAt: "2023-12-09T12:22:30.356Z") + // Arrange: Create multiple ToDo items with varying attributes. + let todo1 = ToDoItemData(id: 0, + title: "Prepare Presentation", + deadline: "2023-05-13T20:49:00.000Z", + status: "todo", + tags: [], + createdAt: "2023-12-01T12:22:30.356Z", + updatedAt: "2023-12-08T12:22:30.356Z") + let todo2 = ToDoItemData(id: 1, + title: "Visit Doctor", + deadline: "2023-05-13T20:49:00.000+08:00", + status: "todo", + tags: [Tag(name: "health")], + createdAt: "2023-12-02T12:22:30.356Z", + updatedAt: "2023-12-10T12:22:30.356Z") + let todo3 = ToDoItemData(id: 2, + title: "Finish Book", + deadline: "2024-12-30T15:12:26.676Z", + status: "todo", + tags: [Tag(name: "reading")], + createdAt: "2023-12-03T12:22:30.356Z", + updatedAt: "2023-12-09T12:22:30.356Z") + let todo4 = ToDoItemData(id: 3, + title: "Finish Book", + deadline: nil, + status: "todo", + tags: [Tag(name: "reading")], + createdAt: "2023-12-03T12:22:30.356Z", + updatedAt: "2023-12-09T12:22:30.356Z") + // Act: Save all ToDo items. try! service.save(todo: todo1) try! service.save(todo: todo2) try! service.save(todo: todo3) try! service.save(todo: todo4) + // Assert: Fetch the items and check their order. let todos = try! service.fetchTodos() #expect(todos.count == 4) - #expect(todos.first!.id == 1) - #expect(todos[1].id == 0) - #expect(todos[2].id == 2) - #expect(todos[3].id == 3) + #expect(todos.first!.id == 1) // Visit Doctor + #expect(todos[1].id == 0) // Prepare Presentation + #expect(todos[2].id == 2) // Finish Book + #expect(todos[3].id == 3) // Finish Book } + /// Tests updating an existing ToDo item. @Test func testUpdateTodo() { + // Arrange: Create and save a ToDo item. let todo = ToDoItemData(id: 1, title: "Draft Report", deadline: nil, status: "todo", tags: [Tag(name: "work")], - createdAt: "now", updatedAt: "now") + createdAt: "now", + updatedAt: "now") try! service.save(todo: todo) + // Act: Update the ToDo item. let updatedTodo = ToDoItem(id: 1, title: "Draft Final Report", deadline: nil, @@ -100,6 +109,7 @@ struct ToDoLocalServiceTests { updatedAt: "now") try! service.update(todoId: 1, newToDo: updatedTodo) + // Assert: Fetch the updated item and verify its properties. let todos = try! service.fetchTodos() let fetchedTodo = todos.first! @@ -108,25 +118,31 @@ struct ToDoLocalServiceTests { #expect(fetchedTodo.tags.count == 0) } + /// Tests deleting a ToDo item. @Test func testDeleteTodo() async throws { + // Arrange: Create and save two ToDo items. let todo1 = ToDoItemData(id: 0, title: "Walk the Dog", - deadline: nil, status: "todo", + deadline: nil, + status: "todo", tags: [Tag(name: "pets")], createdAt: "now", updatedAt: "now") let todo2 = ToDoItemData(id: 1, title: "Walk the Dog", - deadline: nil, status: "todo", + deadline: nil, + status: "todo", tags: [Tag(name: "pets")], createdAt: "now", updatedAt: "now") try service.save(todo: todo1) try service.save(todo: todo2) + + // Act: Delete the second ToDo item. try service.delete(todo: todo2) - // Delete non-existent todo, expect no change + // Attempt to delete a non-existent ToDo item, expecting no change. let todo3 = ToDoItemData(id: 2, title: "Walk the Dog", deadline: nil, @@ -136,7 +152,10 @@ struct ToDoLocalServiceTests { updatedAt: "now") try service.delete(todo: todo3) + // Assert: Check the remaining ToDo items. let todos = try service.fetchTodos() + + // Only one ToDo should remain. #expect(todos.count == 1) } } diff --git a/mobile-client/ToDoListTests/ToDoReducerTests.swift b/mobile-client/ToDoListTests/ToDoReducerTests.swift index 65bf342..52d929c 100644 --- a/mobile-client/ToDoListTests/ToDoReducerTests.swift +++ b/mobile-client/ToDoListTests/ToDoReducerTests.swift @@ -5,6 +5,7 @@ import Testing @testable import ToDoList struct ToDoReducerTests { + /// Tests the successful fetching of ToDo items from the remote service. @Test func testFetchToDosSuccess() async { let todos = [ @@ -21,16 +22,19 @@ struct ToDoReducerTests { $0.toDoService = mockService } + // Send the fetch action and expect loading state to be true. await store.send(.fetchToDos) { $0.isLoading = true } + // Receive the successful fetch response and update the state accordingly. await store.receive(.fetchToDosResponse(.success(todos))) { $0.isLoading = false $0.todos = IdentifiedArrayOf(uniqueElements: todos) } } + /// Tests the failure case when fetching ToDo items from the remote service. @Test func testFetchToDosFailure() async { let mockService = MockToDoRemoteService() @@ -42,10 +46,12 @@ struct ToDoReducerTests { $0.toDoService = mockService } + // Send the fetch action and expect loading state to be true. await store.send(.fetchToDos) { $0.isLoading = true } + // Receive the failure response and update the state with an error message. await store.receive(.fetchToDosResponse(.failure(.networkError(mockService.error!)))) { $0.isLoading = false $0.error = "Network error: The operation couldn’t be completed. (Fail error 0.)" @@ -67,6 +73,7 @@ struct ToDoReducerTests { } } + /// Tests the successful addition of a new ToDo item. @Test func testAddToDoSuccess() async { let mockService = MockToDoRemoteService() @@ -78,11 +85,12 @@ struct ToDoReducerTests { $0.toDoService = mockService } + // Trigger the add button action and initialize the addToDo state. await store.send(.addButtonTapped) { $0.addToDo = AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: "")) } - // We not only test addition value, but only insertion index + // Prepare the first ToDo item and send the save response. let todo1 = ToDoItem(id: 1, title: "Task 1", deadline: "2022-10-10T10:00:00.807Z", status: "pending", tags: [], createdAt: "2024-11-01T10:00:00.807Z", updatedAt: "2024-11-01T10:00:00.117Z") await store.send(.addToDo(.presented(.saveResponse(.success(todo1))))) { $0.insertionIndex = 0 @@ -90,8 +98,8 @@ struct ToDoReducerTests { $0.addToDo = nil } + // Prepare and add the second ToDo item. let todo2 = ToDoItem(id: 2, title: "Task 2", deadline: "2024-10-05T10:00:00.807Z", status: "pending", tags: [], createdAt: "2024-10-01T10:00:00.807Z", updatedAt: "2024-10-01T10:00:00.807Z") - await store.send(.addButtonTapped) { $0.addToDo = AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: "")) } @@ -102,6 +110,7 @@ struct ToDoReducerTests { $0.addToDo = nil } + // Prepare and add the third ToDo item. let todo3 = ToDoItem(id: 3, title: "Task 3", deadline: "2021-10-10T10:00:00.807Z", status: "pending", tags: [], createdAt: "2024-10-01T11:00:00.807Z", updatedAt: "2024-10-01T11:00:00.807Z") await store.send(.addButtonTapped) { $0.addToDo = AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: "")) @@ -113,6 +122,7 @@ struct ToDoReducerTests { $0.addToDo = nil } + // Prepare and add the fourth ToDo item. let todo4 = ToDoItem(id: 4, title: "Task 4", deadline: nil, status: "pending", tags: [], createdAt: "2024-10-01T11:00:00.807Z", updatedAt: "2024-10-01T11:00:00.807Z") await store.send(.addButtonTapped) { $0.addToDo = AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: "")) @@ -125,10 +135,12 @@ struct ToDoReducerTests { } } + /// Tests the performance of inserting a new ToDo item into the list. @Test func testInsertingNewToDoItemPerformance() async { let totalAdditionDay = 6000 + // Generate a list of ToDo items to be added. var todos = (0 ..< totalAdditionDay).map { i in ToDoItem( id: i, @@ -141,11 +153,12 @@ struct ToDoReducerTests { ) } + // Sort ToDo items by their deadline. todos.sort { ($0.deadline.flatMap(ToDoDateFormatter.isoDateFormatter.date(from:)) ?? Date.distantFuture) < ($1.deadline.flatMap(ToDoDateFormatter.isoDateFormatter.date(from:)) ?? Date.distantFuture) } - + // Select a random deadline for the new ToDo item to be inserted between two existing items. let inBetweenDeadline = randomDeadlineInBetween(between: todos[4217].deadline!, and: todos[4218].deadline!) let initialState = ToDoListReducer.State(todos: IdentifiedArrayOf(uniqueElements: todos)) @@ -156,6 +169,7 @@ struct ToDoReducerTests { $0.toDoService = MockToDoRemoteService() } + // Create a new ToDo item with a specified deadline. let newTodo = ToDoItem( id: totalAdditionDay, title: "New Task", @@ -170,8 +184,10 @@ struct ToDoReducerTests { $0.addToDo = AddToDoReducer.State(todo: ToDoItem(id: 0, title: "", deadline: nil, status: "", tags: [], createdAt: "", updatedAt: "")) } + // Record the start time of the insertion operation let runStartTime = Date().millisecondsSince1970 + // Measure the performance of adding a new ToDo item. await store.send(.addToDo(.presented(.saveResponse(.success(newTodo))))) { $0.insertionIndex = 4218 $0.todos.insert(newTodo, at: $0.insertionIndex) @@ -188,6 +204,7 @@ struct ToDoReducerTests { #expect(spentTime < 130) } + /// Tests the successful deletion of a ToDo item. @Test func testDeleteToDoSuccess() async { let todos = [ @@ -216,11 +233,12 @@ struct ToDoReducerTests { } } + /// Tests the failure case when trying to delete a ToDo item. @Test func testDeleteToDoFailure() async { let todos = [ - ToDoItem(id: 1, title: "Task 1", status: "in-progress", tags: [], createdAt: "2024-10-10T08:30:00.117Z", updatedAt: "2024-10-20T10:15:00.117Z"), - ToDoItem(id: 2, title: "Task 2", status: "pending", tags: [], createdAt: "2024-10-15T09:00:00.117Z", updatedAt: "2024-10-25T14:00:00.117Z"), + ToDoItem(id: 1, title: "Task 1", deadline: nil, status: "in-progress", tags: [], createdAt: "2024-10-10T08:30:00.807Z", updatedAt: "2024-10-20T10:15:00.807Z"), + ToDoItem(id: 2, title: "Task 2", deadline: nil, status: "pending", tags: [], createdAt: "2024-10-15T09:00:00.807Z", updatedAt: "2024-10-25T14:00:00.807Z"), ] let mockService = MockToDoRemoteService() @@ -233,11 +251,13 @@ struct ToDoReducerTests { $0.toDoService = mockService } + // Send the delete action for the second ToDo item and expect the deletion process to start. await store.send(.deleteToDoItem(1)) { $0.isDeleting = true $0.deletingTodoID = 1 } + // Receive the failure response and update the state to reflect the error. await store.receive(.deleteToDoResponse(.failure(.networkError(mockService.error!)))) { $0.isDeleting = false $0.deletingTodoID = nil @@ -247,6 +267,11 @@ struct ToDoReducerTests { } extension ToDoReducerTests { + /// Generates a random deadline string in ISO 8601 format by adding a random number of days + /// to the current date, constrained by the specified maximum number of days to add. + /// + /// - Parameter additionDay: The maximum number of days to add to the current date. + /// - Returns: A string representing the randomly generated deadline in ISO 8601 format. func randomDeadline(additionDay: Int) -> String { let calendar = Calendar.current let currentDate = Date() @@ -261,6 +286,13 @@ extension ToDoReducerTests { return formatter.string(from: randomDate) } + /// Generates a random deadline string in ISO 8601 format between two specified date strings. + /// + /// - Parameters: + /// - startString: A string representing the start date in ISO 8601 format. + /// - endString: A string representing the end date in ISO 8601 format. + /// - Returns: An optional string representing a randomly generated deadline in ISO 8601 format, + /// or `nil` if the input date strings cannot be parsed. func randomDeadlineInBetween(between startString: String, and endString: String) -> String? { guard let startDate = ToDoDateFormatter.isoDateFormatter.date(from: startString), @@ -269,6 +301,7 @@ extension ToDoReducerTests { return nil } + // Calculate the time interval between the start and end dates let timeInterval = endDate.timeIntervalSince(startDate) let randomTimeInterval = TimeInterval.random(in: 0 ... timeInterval) let randomDate = startDate.addingTimeInterval(randomTimeInterval) @@ -278,6 +311,7 @@ extension ToDoReducerTests { } extension Date { + /// A computed property that returns the time interval since 1970 in milliseconds. var millisecondsSince1970: Int64 { Int64((timeIntervalSince1970 * 1000.0).rounded()) } diff --git a/mobile-client/ToDoListTests/ToDoRemoveServiceTests.swift b/mobile-client/ToDoListTests/ToDoRemoveServiceTests.swift index 8046800..384cc54 100644 --- a/mobile-client/ToDoListTests/ToDoRemoveServiceTests.swift +++ b/mobile-client/ToDoListTests/ToDoRemoveServiceTests.swift @@ -3,16 +3,23 @@ import Testing @testable import ToDoList struct ToDoRemoteServiceTests { + // A mock data fetcher to simulate network responses. private var mockDataFetcher: MockDataFetcher + + // A mock local service to simulate local data storage. private var mockLocalService: MockToDoLocalService + + // The service under test, using the mocked dependencies. private var service: ToDoRemoteService + /// Initializes the test suite, setting up mock services for testing. init() { mockLocalService = MockToDoLocalService() mockDataFetcher = MockDataFetcher() service = ToDoRemoteService(localService: mockLocalService, dataFetcher: mockDataFetcher) } + /// Tests the successful fetching of ToDo items from the remote service. @Test func testFetchDoToSuccess() async { let todos = [ @@ -47,18 +54,29 @@ struct ToDoRemoteServiceTests { createdAt: "2024-07-22T13:45:00.117Z", updatedAt: "2024-08-30T13:45:00.117Z"), ] + // Prepare the response to simulate a successful fetch. let fetchResponse = FetchToDoResponse(success: true, data: todos) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the fetch request. let url = URL(string: "http://localhost:5000/todos")! + + // Create a successful HTTP response. let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher. mockDataFetcher.result = (jsonData, httpResponse) + // Call the method to fetch ToDo items. let remoteToDos = try! await service.fetchToDos() + // Expect the number of fetched ToDos to match the mock data. #expect(remoteToDos.count == todos.count) } + /// Tests the order of fetched ToDo items from the remote service. @Test func testFetchToDoSuccessFetchOrder() async { let todos = [ @@ -99,13 +117,21 @@ struct ToDoRemoteServiceTests { updatedAt: "2024-08-30T13:45:00.222Z"), ] + // Prepare the response to simulate a successful fetch. let fetchResponse = FetchToDoResponse(success: true, data: todos) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the fetch request. let url = URL(string: "http://localhost:5000/todos")! + + // Create a successful HTTP response. let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! mockDataFetcher.result = (jsonData, httpResponse) + // Call the method to fetch ToDo items and validate the order of fetched ToDo items. let remoteToDos = try! await service.fetchToDos() #expect(remoteToDos[0].id == 2) @@ -115,67 +141,112 @@ struct ToDoRemoteServiceTests { #expect(remoteToDos[4].deadline == nil) } + /// Tests the failure scenario when fetching ToDo items from the remote service. @Test func testFetchDoToFailure() async { + // Define the URL for the fetch request. let url = URL(string: "http://localhost:5000/todos")! + + // Create an HTTP response with a status code indicating failure (500 Internal Server Error). let httpResponse = HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a failure response. mockDataFetcher.result = (Data(), httpResponse) do { + // Attempt to fetch ToDo items, expecting an error to be thrown. _ = try await service.fetchToDos() + + // If no error is thrown, record an unexpected error. Issue.record("Unexpected Error") } catch { + // Handle the error thrown by the fetch method. if let serviceError = error as? ToDoServiceError { + // Expect the error to be of type ToDoServiceError and verify the status code. #expect(serviceError == ToDoServiceError.invalidResponse(500)) } else { + // Record an unexpected error if the error type does not match. Issue.record("Unexpected Error") } } } + /// Tests the successful posting of a ToDo item to the remote service. @Test func testPostToDoSuccess() async { + // Create a sample ToDo item to be posted. let todo = ToDoItem(id: 1, title: "Buy groceries", status: "pending", tags: ["errand"], createdAt: "2024-10-10T10:00:00.117Z", updatedAt: "2024-10-15T10:00:00.117Z") + + // Prepare the response to simulate a successful post. let fetchResponse = AddToDoResponse(success: true, data: todo) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the post request. let url = URL(string: "http://localhost:5000/todos")! + + // Create a successful HTTP response. let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a successful post response. mockDataFetcher.result = (jsonData, httpResponse) + do { + // Call the method to post the ToDo item. let remoteToDo = try await service.postToDo(todo) + + // Verify the title of the posted ToDo matches the original. #expect(remoteToDo.title == todo.title) + + // Expect the local service's todos count to be 1 after the post. #expect(mockLocalService.todos.count == 1) + + // Verify the createdAt property of the stored ToDo matches the original. #expect(mockLocalService.todos.first!.createdAt == todo.createdAt) } catch { + // Record an unexpected error if one occurs. Issue.record("Unexpected Error") } } + /// Tests the failure of posting a ToDo item to the remote service. @Test func testPostToDoFailure() async { + // Create a sample ToDo item to be posted. let todo = ToDoItem(id: 1, title: "Buy groceries", status: "pending", tags: ["errand"], createdAt: "2024-10-10T10:00:00.333Z", updatedAt: "2024-10-15T10:00:00.333Z") + + // Prepare the response to simulate a successful post. let fetchResponse = AddToDoResponse(success: true, data: todo) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the post request. let url = URL(string: "http://localhost:5000/todos")! + + // Create an HTTP response with a 500 status code to simulate a server error. let httpResponse = HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a server error. mockDataFetcher.result = (jsonData, httpResponse) + do { + // Attempt to post the ToDo item. _ = try await service.postToDo(todo) Issue.record("Unexpected Error") } catch { + // Verify that the error returned is of the expected type. if let serviceError = error as? ToDoServiceError { #expect(serviceError == ToDoServiceError.invalidResponse(500)) } else { @@ -184,61 +255,97 @@ struct ToDoRemoteServiceTests { } } + /// Tests the successful deletion of a ToDo item from the remote service. @Test func testDeleteToDoSuccess() async { + // Create a sample ToDo item to be posted and deleted. let todo = ToDoItem(id: 1, title: "Buy groceries", status: "pending", tags: ["errand"], createdAt: "2024-10-10T10:00:00.333Z", updatedAt: "2024-10-15T10:00:00.333Z") + + // Prepare the response to simulate a successful post. let fetchResponse = AddToDoResponse(success: true, data: todo) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the post request. let url = URL(string: "http://localhost:5000/todos")! + + // Create a successful HTTP response. let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a successful post response. mockDataFetcher.result = (jsonData, httpResponse) + // Post the ToDo item. let remoteToDo = try! await service.postToDo(todo) + // Verify the title of the posted ToDo matches the original. #expect(remoteToDo.title == todo.title) + // Expect the local service's todos count to be 1 after the post. #expect(mockLocalService.todos.count == 1) + // Set the result of the mock data fetcher to simulate a successful deletion. mockDataFetcher.result = (Data(), httpResponse) + do { + // Attempt to delete the ToDo item. try await service.deleteToDo(id: 1) + // Expect the local service's todos count to be 0 after the deletion. #expect(mockLocalService.todos.count == 0) } catch { Issue.record("Unexpected Error") } } + /// Tests the failure of deleting a ToDo item from the remote service. @Test func testDeleteToDoFailure() async { + // Create a sample ToDo item to be posted and deleted. let todo = ToDoItem(id: 1, title: "Buy groceries", status: "pending", tags: ["errand"], createdAt: "2024-10-10T10:00:00.333Z", updatedAt: "2024-10-15T10:00:00.333Z") + + // Prepare the response to simulate a successful post. let fetchResponse = AddToDoResponse(success: true, data: todo) + + // Encode the response into JSON data. let jsonData = try! JSONEncoder().encode(fetchResponse) + + // Define the URL for the post request. let url = URL(string: "http://localhost:5000/todos")! + + // Create a successful HTTP response. var httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a successful post response. mockDataFetcher.result = (jsonData, httpResponse) + // Post the ToDo item. let remoteToDo = try! await service.postToDo(todo) + // Verify the title of the posted ToDo matches the original. #expect(remoteToDo.title == todo.title) + // Expect the local service's todos count to be 1 after the post. #expect(mockLocalService.todos.count == 1) + // Create an HTTP response with a 500 status code to simulate a server error for deletion. httpResponse = HTTPURLResponse(url: url, statusCode: 500, httpVersion: nil, headerFields: nil)! mockDataFetcher.result = (Data(), httpResponse) + do { + // Attempt to delete the ToDo item. try await service.deleteToDo(id: 1) Issue.record("Unexpected Error") } catch { + // Verify that the error returned is of the expected type. if let serviceError = error as? ToDoServiceError { #expect(serviceError == ToDoServiceError.invalidResponse(500)) } else { @@ -247,8 +354,10 @@ struct ToDoRemoteServiceTests { } } + /// Tests the synchronization of remote and local ToDo items during updates and removals. @Test func testRemoteLocalSyncUpdateRemove() async { + // Populate the local service with existing ToDo items. mockLocalService.todos = [ ToDoItemData(id: 1, title: "Local Title", @@ -280,6 +389,7 @@ struct ToDoRemoteServiceTests { updatedAt: "2024-11-07T09:30:00.333Z"), ] + // Create mock remote ToDo items to fetch. let todos = [ ToDoItem(id: 1, title: "New Title", @@ -294,21 +404,26 @@ struct ToDoRemoteServiceTests { createdAt: "2024-10-01T09:00:00.333Z", updatedAt: "2024-10-12T09:00:00.333Z"), ] + + // Prepare the response to simulate a successful fetch. let fetchResponse = FetchToDoResponse(success: true, data: todos) let jsonData = try! JSONEncoder().encode(fetchResponse) let url = URL(string: "http://localhost:5000/todos")! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a successful fetch response. mockDataFetcher.result = (jsonData, httpResponse) + // Fetch the remote ToDo items. let remoteToDos = try! await service.fetchToDos() + // Verify that the number of fetched ToDos matches the mock data. #expect(remoteToDos.count == todos.count) - // Local number of todo item should be same as the latest fetch number + // Verify that the local service's todos count matches the fetched count. #expect(mockLocalService.todos.count == todos.count) - // Title of local item with id 1 should be updated + // Verify the title of the local item with id 1 is updated to match the remote item. if let localTodoWithId1 = mockLocalService.todos.first(where: { $0.id == 1 }), let remoteTodoWithId1 = remoteToDos.first(where: { $0.id == 1 }) { @@ -320,8 +435,10 @@ struct ToDoRemoteServiceTests { } } + /// Tests the synchronization of remote ToDo items with local storage, ensuring updates and additions work correctly. @Test func testRemoteLocalSyncUpdateAdd() async { + // Initial local ToDo items. let originLocalToDos = [ ToDoItemData(id: 1, title: "Local Title", @@ -339,8 +456,10 @@ struct ToDoRemoteServiceTests { updatedAt: "2024-10-19T14:45:00.333Z"), ] + // Set the local service's ToDo items. mockLocalService.todos = originLocalToDos + // Mock remote ToDo items to fetch. let todos = [ ToDoItem(id: 1, title: "New Title", @@ -369,21 +488,26 @@ struct ToDoRemoteServiceTests { createdAt: "2024-09-25T14:00:00.323Z", updatedAt: "2024-11-07T09:30:00.323Z"), ] + + // Prepare the response to simulate a successful fetch. let fetchResponse = FetchToDoResponse(success: true, data: todos) let jsonData = try! JSONEncoder().encode(fetchResponse) let url = URL(string: "http://localhost:5000/todos")! let httpResponse = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil)! + // Set the result of the mock data fetcher to simulate a successful fetch response. mockDataFetcher.result = (jsonData, httpResponse) + // Fetch remote ToDo items. let remoteToDos = try! await service.fetchToDos() + // Check that the number of remote ToDos matches the expected count. #expect(remoteToDos.count == todos.count) - // Latest number of local todo item should be greater than origin todos + // The local ToDo count should be greater than the original local count after sync. #expect(mockLocalService.todos.count > originLocalToDos.count) - // Title of local item with id 1 should be updated + // Check that the local item with id 2 has been updated to match the remote item. if let localTodoWithId2 = mockLocalService.todos.first(where: { $0.id == 2 }), let remoteTodoWithId2 = remoteToDos.first(where: { $0.id == 2 }) { @@ -391,7 +515,7 @@ struct ToDoRemoteServiceTests { #expect(localTodoWithId2.tags.count == remoteTodoWithId2.tags.count) #expect(localTodoWithId2.tags.first!.name == remoteTodoWithId2.tags.first!) } else { - Issue.record("Unexpected Error") + Issue.record("Unexpected Error: Could not find local or remote ToDo item with ID 2") } } }