Skip to content

Commit 1bb9ebd

Browse files
committed
feat: add tests for request caching service (TBD54566975#280)
1 parent ca5ac62 commit 1bb9ebd

File tree

3 files changed

+253
-2
lines changed

3 files changed

+253
-2
lines changed

lib/shared/services/request_cache_service.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,15 @@ class RequestCacheService<T> {
1010
final Map<String, dynamic> Function(T) toJson;
1111
final Duration cacheDuration;
1212
final String networkCacheKey;
13+
final http.Client httpClient;
1314

1415
RequestCacheService({
1516
required this.fromJson,
1617
required this.toJson,
1718
this.cacheDuration = const Duration(minutes: 1),
1819
this.networkCacheKey = 'network_cache',
19-
});
20+
http.Client? httpClient,
21+
}) : httpClient = httpClient ?? http.Client();
2022

2123
/// Fetches data from the cache and API.
2224
Stream<T> fetchData(String url) async* {
@@ -48,7 +50,7 @@ class RequestCacheService<T> {
4850

4951
// Fetch data from the network
5052
try {
51-
final response = await http.get(Uri.parse(url));
53+
final response = await httpClient.get(Uri.parse(url));
5254

5355
if (response.statusCode == 200) {
5456
final dataMap = jsonDecode(response.body) as Map<String, dynamic>;

pubspec.yaml

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ dependencies:
4444
url_launcher: ^6.2.5
4545
webview_flutter: ^4.4.2
4646
web5: ^0.4.0
47+
hive_test: ^1.0.1
4748

4849
dev_dependencies:
4950
flutter_test:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
import 'dart:convert';
2+
3+
import 'package:collection/collection.dart';
4+
import 'package:didpay/shared/services/request_cache_service.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
import 'package:hive/hive.dart';
7+
import 'package:hive_test/hive_test.dart';
8+
import 'package:http/http.dart' as http;
9+
import 'package:http/testing.dart';
10+
11+
class TestData {
12+
final int id;
13+
final String name;
14+
15+
TestData({required this.id, required this.name});
16+
17+
factory TestData.fromJson(Map<String, dynamic> json) {
18+
return TestData(
19+
id: json['id'] as int,
20+
name: json['name'] as String,
21+
);
22+
}
23+
24+
Map<String, dynamic> toJson() => {
25+
'id': id,
26+
'name': name,
27+
};
28+
}
29+
30+
void main() {
31+
// Initialize in-memory Hive storage before tests
32+
setUp(() async {
33+
await setUpTestHive();
34+
});
35+
36+
// Clean up after tests
37+
tearDown(() async {
38+
await tearDownTestHive();
39+
});
40+
41+
group('RequestCacheService Tests', () {
42+
test('fetchData emits data from network when no cache exists', () async {
43+
// Arrange
44+
const testUrl = 'https://example.com/data';
45+
final mockResponseData = {'id': 1, 'name': 'Test Item'};
46+
final mockClient = MockClient((request) async {
47+
if (request.url.toString() == testUrl) {
48+
return http.Response(jsonEncode(mockResponseData), 200);
49+
}
50+
return http.Response('Not Found', 404);
51+
});
52+
53+
final service = RequestCacheService<TestData>(
54+
fromJson: TestData.fromJson,
55+
toJson: (data) => data.toJson(),
56+
httpClient: mockClient,
57+
);
58+
59+
final dataStream = service.fetchData(testUrl);
60+
61+
await expectLater(
62+
dataStream,
63+
emits(predicate<TestData>((data) =>
64+
data.id == mockResponseData['id'] &&
65+
data.name == mockResponseData['name'])),
66+
);
67+
});
68+
69+
test(
70+
'fetchData emits cached data when cache is valid and data is unchanged',
71+
() async {
72+
// Arrange
73+
const testUrl = 'https://example.com/data';
74+
final mockCachedData = {'id': 1, 'name': 'Cached Item'};
75+
final now = DateTime.now();
76+
77+
// Pre-populate the Hive box with cached data
78+
final box = await Hive.openBox('network_cache');
79+
final cacheKey = testUrl.hashCode.toString();
80+
await box.put(cacheKey, {
81+
'data': mockCachedData,
82+
'timestamp': now.toIso8601String(),
83+
});
84+
85+
// Mock HTTP client returns the same data as cached
86+
final mockClient = MockClient((request) async {
87+
return http.Response(jsonEncode(mockCachedData), 200);
88+
});
89+
90+
final service = RequestCacheService<TestData>(
91+
fromJson: TestData.fromJson,
92+
toJson: (data) => data.toJson(),
93+
httpClient: mockClient,
94+
);
95+
96+
final dataStream = service.fetchData(testUrl);
97+
98+
await expectLater(
99+
dataStream,
100+
emits(predicate<TestData>((data) =>
101+
data.id == mockCachedData['id'] &&
102+
data.name == mockCachedData['name'])),
103+
);
104+
});
105+
106+
test(
107+
'fetchData emits new data when network data is different from cached data',
108+
() async {
109+
// Arrange
110+
const testUrl = 'https://example.com/data';
111+
final mockCachedData = {'id': 1, 'name': 'Old Item'};
112+
final mockResponseData = {'id': 1, 'name': 'Updated Item'};
113+
final now = DateTime.now();
114+
115+
// Pre-populate the Hive box with old cached data
116+
final box = await Hive.openBox('network_cache');
117+
final cacheKey = testUrl.hashCode.toString();
118+
await box.put(cacheKey, {
119+
'data': mockCachedData,
120+
'timestamp': now.toIso8601String(),
121+
});
122+
123+
// Mock HTTP client returns updated data
124+
final mockClient = MockClient((request) async {
125+
return http.Response(jsonEncode(mockResponseData), 200);
126+
});
127+
128+
final service = RequestCacheService<TestData>(
129+
fromJson: TestData.fromJson,
130+
toJson: (data) => data.toJson(),
131+
httpClient: mockClient,
132+
);
133+
134+
final dataStream = service.fetchData(testUrl);
135+
136+
await expectLater(
137+
dataStream,
138+
emitsInOrder([
139+
predicate<TestData>((data) =>
140+
data.id == mockCachedData['id'] &&
141+
data.name == mockCachedData['name']),
142+
predicate<TestData>((data) =>
143+
data.id == mockResponseData['id'] &&
144+
data.name == mockResponseData['name']),
145+
]),
146+
);
147+
});
148+
149+
test('fetchData throws error when no cache exists and network fails',
150+
() async {
151+
// Arrange
152+
const testUrl = 'https://example.com/data';
153+
154+
// Mock HTTP client returns an error
155+
final mockClient = MockClient((request) async {
156+
return http.Response('Server Error', 500);
157+
});
158+
159+
final service = RequestCacheService<TestData>(
160+
fromJson: TestData.fromJson,
161+
toJson: (data) => data.toJson(),
162+
httpClient: mockClient,
163+
);
164+
165+
expect(
166+
service.fetchData(testUrl),
167+
emitsError(isA<Exception>()),
168+
);
169+
});
170+
171+
test('fetchData emits cached data when network fails', () async {
172+
// Arrange
173+
const testUrl = 'https://example.com/data';
174+
final mockCachedData = {'id': 1, 'name': 'Cached Item'};
175+
final now = DateTime.now();
176+
177+
// Pre-populate the Hive box with cached data
178+
final box = await Hive.openBox('network_cache');
179+
final cacheKey = testUrl.hashCode.toString();
180+
await box.put(cacheKey, {
181+
'data': mockCachedData,
182+
'timestamp': now.toIso8601String(),
183+
});
184+
185+
// Mock HTTP client returns an error
186+
final mockClient = MockClient((request) async {
187+
return http.Response('Server Error', 500);
188+
});
189+
190+
final service = RequestCacheService<TestData>(
191+
fromJson: TestData.fromJson,
192+
toJson: (data) => data.toJson(),
193+
httpClient: mockClient,
194+
);
195+
196+
final dataStream = service.fetchData(testUrl);
197+
198+
await expectLater(
199+
dataStream,
200+
emits(predicate<TestData>((data) =>
201+
data.id == mockCachedData['id'] &&
202+
data.name == mockCachedData['name'])),
203+
);
204+
});
205+
206+
test('fetchData fetches new data when cache is expired', () async {
207+
// Arrange
208+
const testUrl = 'https://example.com/data';
209+
final mockCachedData = {'id': 1, 'name': 'Old Item'};
210+
final mockResponseData = {'id': 1, 'name': 'New Item'};
211+
final expiredTime = DateTime.now().subtract(Duration(minutes: 10));
212+
213+
// Pre-populate the Hive box with expired cached data
214+
final box = await Hive.openBox('network_cache');
215+
final cacheKey = testUrl.hashCode.toString();
216+
await box.put(cacheKey, {
217+
'data': mockCachedData,
218+
'timestamp': expiredTime.toIso8601String(),
219+
});
220+
221+
// Mock HTTP client returns new data
222+
final mockClient = MockClient((request) async {
223+
return http.Response(jsonEncode(mockResponseData), 200);
224+
});
225+
226+
final service = RequestCacheService<TestData>(
227+
fromJson: TestData.fromJson,
228+
toJson: (data) => data.toJson(),
229+
httpClient: mockClient,
230+
cacheDuration: Duration(minutes: 1), // Set cache duration for the test
231+
);
232+
233+
final dataStream = service.fetchData(testUrl);
234+
235+
await expectLater(
236+
dataStream,
237+
emitsInOrder([
238+
predicate<TestData>((data) =>
239+
data.id == mockCachedData['id'] &&
240+
data.name == mockCachedData['name']),
241+
predicate<TestData>((data) =>
242+
data.id == mockResponseData['id'] &&
243+
data.name == mockResponseData['name']),
244+
]),
245+
);
246+
});
247+
});
248+
}

0 commit comments

Comments
 (0)