GlucoseStorageTests.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import CoreData
  2. import Foundation
  3. import Swinject
  4. import Testing
  5. @testable import Trio
  6. @Suite("GlucoseStorage Tests", .serialized) struct GlucoseStorageTests: Injectable {
  7. @Injected() var storage: GlucoseStorage!
  8. let resolver: Resolver
  9. var coreDataStack: CoreDataStack!
  10. var testContext: NSManagedObjectContext!
  11. init() async throws {
  12. // Create test context
  13. // As we are only using this single test context to initialize our in-memory DeterminationStorage we need to perform the Unit Tests serialized
  14. coreDataStack = try await CoreDataStack.createForTests()
  15. testContext = coreDataStack.newTaskContext()
  16. // Create assembler with test assembly
  17. let assembler = Assembler([
  18. StorageAssembly(),
  19. ServiceAssembly(),
  20. APSAssembly(),
  21. NetworkAssembly(),
  22. UIAssembly(),
  23. SecurityAssembly(),
  24. TestAssembly(testContext: testContext) // Add our test assembly last to override Storage
  25. ])
  26. resolver = assembler.resolver
  27. injectServices(resolver)
  28. }
  29. @Test("Storage is correctly initialized") func testStorageInitialization() {
  30. // Verify storage exists
  31. #expect(storage != nil, "GlucoseStorage should be injected")
  32. // Verify it's the correct type
  33. #expect(storage is BaseGlucoseStorage, "Storage should be of type BaseGlucoseStorage")
  34. }
  35. @Test("Store and retrieve glucose entries") func testStoreAndRetrieveGlucose() async throws {
  36. // Given
  37. let testGlucose = [
  38. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 126)
  39. ]
  40. // When
  41. try await storage.storeGlucose(testGlucose)
  42. // Then verify stored entries
  43. let storedEntries = try await coreDataStack.fetchEntitiesAsync(
  44. ofType: GlucoseStored.self,
  45. onContext: testContext,
  46. predicate: NSPredicate(format: "glucose == 126"),
  47. key: "date",
  48. ascending: false
  49. ) as? [GlucoseStored]
  50. #expect(storedEntries?.isEmpty == false, "Should have stored entries")
  51. #expect(storedEntries?.count == 1, "Should have exactly one entry")
  52. #expect(storedEntries?[0].glucose == 126, "Glucose value should match")
  53. #expect(storedEntries?[0].direction == "Flat", "Direction should match")
  54. }
  55. @Test("Delete glucose entry") func testDeleteGlucose() async throws {
  56. // Given
  57. let testGlucose = [
  58. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 140)
  59. ]
  60. try await storage.storeGlucose(testGlucose)
  61. // Get the stored entry's ObjectID
  62. let storedEntries = try await coreDataStack.fetchEntitiesAsync(
  63. ofType: GlucoseStored.self,
  64. onContext: testContext,
  65. predicate: NSPredicate(format: "glucose == 140"),
  66. key: "date",
  67. ascending: false
  68. ) as? [GlucoseStored]
  69. guard let objectID = storedEntries?.first?.objectID else {
  70. throw TestError("Failed to get stored entry's ObjectID")
  71. }
  72. #expect(storedEntries.isNotNilNotEmpty == true, "Should have exactly one (test) entry")
  73. // When
  74. await storage.deleteGlucose(objectID)
  75. // Then verify deletion
  76. let remainingEntries = try await coreDataStack.fetchEntitiesAsync(
  77. ofType: GlucoseStored.self,
  78. onContext: testContext,
  79. predicate: NSPredicate(format: "glucose == 140"),
  80. key: "date",
  81. ascending: false
  82. ) as? [GlucoseStored]
  83. #expect(remainingEntries?.isEmpty == true, "Should have no entries after deletion")
  84. }
  85. @Test("Get glucose not yet uploaded to Nightscout") func testGetGlucoseNotYetUploadedToNightscout() async throws {
  86. // Given
  87. let testGlucose = [
  88. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 160)
  89. ]
  90. try await storage.storeGlucose(testGlucose)
  91. // When
  92. let notUploadedEntries = try await storage.getGlucoseNotYetUploadedToNightscout()
  93. // Then
  94. #expect(!notUploadedEntries.isEmpty, "Should have entries not uploaded to NS")
  95. #expect(notUploadedEntries[0].glucose == 160, "Glucose value should match")
  96. }
  97. @Test("Get manual glucose not yet uploaded to Nightscout") func testGetManualGlucoseNotYetUploadedToNightscout() async throws {
  98. // Given
  99. storage.addManualGlucose(glucose: 180)
  100. // When
  101. let notUploadedEntries = try await storage.getManualGlucoseNotYetUploadedToNightscout()
  102. // Then
  103. #expect(!notUploadedEntries.isEmpty, "Should have manual entries not uploaded to NS")
  104. let entry = notUploadedEntries[0]
  105. #expect(entry.glucose == "180", "Glucose value should match")
  106. #expect(entry.glucoseType == "Manual", "Type should be mbg for manual entries")
  107. #expect(entry.eventType == .capillaryGlucose, "Type should be capillaryGlucose")
  108. }
  109. @Test(
  110. "Test glucose alarms",
  111. .enabled(if: false, "Flaky test, disabled while investigating")
  112. ) func testGlucoseAlarms() async throws {
  113. // Given
  114. let lowGlucose = [
  115. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 55)
  116. ]
  117. let highGlucose = [
  118. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 271)
  119. ]
  120. let normalGlucose = [
  121. BloodGlucose(direction: BloodGlucose.Direction.flat, date: 123, dateString: Date(), glucose: 100)
  122. ]
  123. // When - Test low glucose
  124. try await storage.storeGlucose(lowGlucose)
  125. var storedEntries = try await coreDataStack.fetchEntitiesAsync(
  126. ofType: GlucoseStored.self,
  127. onContext: testContext,
  128. predicate: NSPredicate(format: "glucose == 55"),
  129. key: "date",
  130. ascending: false
  131. ) as? [GlucoseStored]
  132. // Then
  133. #expect(storedEntries?.first?.glucose == 55, "Low glucose value should match")
  134. #expect(storage.alarm == .low, "Should trigger low glucose alarm") // default low limit is 72 mg/dL
  135. // When - Test high glucose
  136. try await storage.storeGlucose(highGlucose)
  137. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  138. ofType: GlucoseStored.self,
  139. onContext: testContext,
  140. predicate: NSPredicate(format: "glucose == 271"),
  141. key: "date",
  142. ascending: false
  143. ) as? [GlucoseStored]
  144. // Then
  145. #expect(storedEntries?.first?.glucose == 271, "High glucose value should match")
  146. #expect(storage.alarm == .high, "Should trigger high glucose alarm") // default high limit is 270 mg/dL
  147. // When - Test normal glucose
  148. try await storage.storeGlucose(normalGlucose)
  149. storedEntries = try await coreDataStack.fetchEntitiesAsync(
  150. ofType: GlucoseStored.self,
  151. onContext: testContext,
  152. predicate: NSPredicate(format: "glucose == 100"),
  153. key: "date",
  154. ascending: false
  155. ) as? [GlucoseStored]
  156. // Then
  157. #expect(storedEntries?.first?.glucose == 100, "Normal glucose value should match")
  158. #expect(storage.alarm == nil, "Should not trigger any alarm")
  159. }
  160. }