Closed ratzrattillo closed 10 months ago
When looking at #539 I don't fully understand what you're trying to achieve. If you want to test the responses returned by the ChopperService
? Do you want to make sure the json is correctly converted?
If I look at the example code I have the feeling that you are mostly testing logic in the test itself. Also it looks like you're testing things that are covered by tests in the Chopper and Freezed packages itself.
If you want a ChopperService
to return a specific model for testing that could easily be accomplished with mockito/mocktail
. For example something like this with mocktail
:
@ChopperApi()
abstract class MyService extends ChopperService {
@Get(path: 'capabilities')
Future<Response<List<DeviceCapabilities>>> getDeviceCapabilities();
}
class MockMyService extends Mock implements MyService{}
void main() {
final mockService = MockMyService();
test('mock call', () async {
when(()=>mockService.getDeviceCapabilities()).thenAnswer((_) async => [DeviceCapabilities(...));
// Do other stuff needed
final result = await mockService.getDeviceCapabilities();
expect(result, expected);
verify(()=>mockService.getDeviceCapabilities()).called(1);
});
}
I also would like to point out that the JsonConverter
does not convert json into an custom object like DeviceCapabilities
. See: https://hadrien-lejard.gitbook.io/chopper/faq#write-a-custom-jsonconverter
Thank you @Guldem , your answer is bringing me closer to understand what I actually want.
I want to test, that the chopper client properly converts the received json response to a custom object. During testing, the responses should not come from the network, but from a file. Another usecase I have is: Replace the underlying httpClient of the ChopperClient with a mocked version, so I can develop my application even if the real backend services are not running.
To fulfill my usecase, would the right method be to move the mocked responses directly into the implementation of class MockMyService like e.g.:?
class MockMyService extends Mock implements MyService{
// when(()=>this.getDeviceCapabilities()).thenAnswer((_) async => [DeviceCapabilities(...));
}
Could this implementation then still be overwritten in tests using the when().thenAnswer() methods?
Thanks for your hint with the converter. It showed me that my assumption was wrong, that the JsonConverter can already convert Json to custom objects. I want to use the converter from https://hadrien-lejard.gitbook.io/chopper/faq#write-a-custom-jsonconverter in my project now. It seems like tests already exist: https://github.com/lejard-h/chopper/blob/master/chopper/test/converter_test.dart
Your example code showed me, that it is easy to change the response of the mockService using the when().thenAnswer() methods. However it seems like the [DeviceCapabilities(...)] should somehow be wrapped in a chopper.Response() and i do not know how to do that. Do you have a hint for me here?
I created a custom class mockHttpClient Builder, which contains all the responses I would expect from the service. This class gives me back an HTTPClient that returns the expected responses, or an error response:
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
class MockHttpClientBuilder {
MockClient generateClient() {
return MockClient(
(request) async {
try {
final response = await _getResponseByPath(request.url.path);
return http.Response(response, 200);
} on PathNotFoundException catch (e) {
return http.Response(e.message, 404);
} catch (e) {
return http.Response('Server Error', 500);
}
},
);
}
MockClient generateErrorClient(String message, int errorcode) {
return MockClient(
(request) async {
return http.Response(message, errorcode);
},
);
}
Future<String> _getResponseByPath(String path) async {
switch (path) {
case '/capabilities':
return await _readResponseFromAsset(
'assets/network/device_capabilities.json');
case '/settings':
return await _readResponseFromAsset(
'assets/network/device_settings.json');
case '/select_device':
return await _readResponseFromAsset(
'assets/network/select_device.json');
case '/create_rx_fg' || '/create_tx_fg':
return await _readResponseFromAsset('assets/network/create_fg.json');
default:
throw PathNotFoundException(path, OSError());
}
}
Future<String> _readResponseFromAsset(String assetPath) async =>
rootBundle.loadString(assetPath);
}
The Converter to transform Json to custom objects i took mostly from https://hadrien-lejard.gitbook.io/chopper/faq#decoding-json-using-isolates
// From: https://hadrien-lejard.gitbook.io/chopper/faq#decoding-json-using-isolates
import 'dart:async' show FutureOr;
import 'dart:convert' show jsonDecode;
import 'package:chopper/chopper.dart' as chopper;
import 'package:my_flutter_app/network/json_decode_service.dart';
import 'package:squadron/squadron.dart';
import 'package:json_annotation/json_annotation.dart';
part 'json_serializable_converter_squadron.g.dart';
void initSquadron(String id) {
Squadron.setId(id);
Squadron.setLogger(ConsoleSquadronLogger());
Squadron.logLevel = SquadronLogLevel.warning; //all
Squadron.debugMode = false; //true
}
@JsonSerializable()
class ResourceError {
final String type;
final String message;
ResourceError(this.type, this.message);
static const fromJsonFactory = _$ResourceErrorFromJson;
Map<String, dynamic> toJson() => _$ResourceErrorToJson(this);
}
typedef JsonFactory<T> = T Function(Map<String, dynamic> json);
/// This JsonConverter works with or without a WorkerPool
class JsonSerializableWorkerPoolConverter extends chopper.JsonConverter {
const JsonSerializableWorkerPoolConverter(this.factories, [this.workerPool]);
final Map<Type, JsonFactory> factories;
final JsonDecodeServiceWorkerPool? workerPool;
T? _decodeMap<T>(Map<String, dynamic> values) {
/// Get jsonFactory using Type parameters
/// if not found or invalid, throw error or return null
final jsonFactory = factories[T];
if (jsonFactory == null || jsonFactory is! JsonFactory<T>) {
/// throw serializer not found error;
return null;
}
return jsonFactory(values);
}
List<T> _decodeList<T>(Iterable values) =>
values.where((v) => v != null).map<T>((v) => _decode<T>(v)).toList();
dynamic _decode<T>(entity) {
if (entity is Iterable) return _decodeList<T>(entity as List);
if (entity is Map) return _decodeMap<T>(entity as Map<String, dynamic>);
return entity;
}
@override
FutureOr<chopper.Response<ResultType>> convertResponse<ResultType, Item>(
chopper.Response response,
) async {
// use [JsonConverter] to decode json
final jsonRes = await super.convertResponse(response);
return jsonRes.copyWith<ResultType>(body: _decode<Item>(jsonRes.body));
}
@override
FutureOr<chopper.Response> convertError<ResultType, Item>(
chopper.Response response) async {
// use [JsonConverter] to decode json
final jsonRes = await super.convertError(response);
return jsonRes.copyWith<ResourceError>(
body: ResourceError.fromJsonFactory(jsonRes.body),
);
}
@override
FutureOr<dynamic> tryDecodeJson(String data) async {
try {
// if there is a worker pool use it, otherwise run in the main thread
return workerPool != null
? await workerPool!.jsonDecode(data)
: jsonDecode(data);
} catch (error) {
print(error);
chopper.chopperLogger.warning(error);
return data;
}
}
}
Now i can run my tests with the mocked http client:
import 'dart:convert';
import 'package:my_flutter_app/network/mock_http_client_builder.dart';
import 'package:squadron/squadron.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:my_flutter_app/network/futuresdr_service.dart';
import 'package:my_flutter_app/network/responses/barrel.dart';
import 'package:my_flutter_app/network/json_decode_service.dart';
import 'package:my_flutter_app/network/json_serializable_converter_squadron.dart';
const String apiUrl = 'http://localhost:4000';
Future<void> main() async {
TestWidgetsFlutterBinding.ensureInitialized();
initSquadron('decode_json_worker_pool');
// Create Worker Pool
final jsonDecodeServiceWorkerPool = JsonDecodeServiceWorkerPool(
concurrencySettings: ConcurrencySettings.oneCpuThread,
);
final converter = JsonSerializableWorkerPoolConverter(
{
DeviceCapabilities: DeviceCapabilities.fromJsonFactory,
ChannelCapabilities: ChannelCapabilities.fromJsonFactory,
DeviceSettings: DeviceSettings.fromJsonFactory,
ChannelSettings: ChannelSettings.fromJsonFactory,
},
// make sure to provide the WorkerPool to the JsonConverter
jsonDecodeServiceWorkerPool,
);
setUp(() async {
await jsonDecodeServiceWorkerPool.start();
});
tearDown(() async {
jsonDecodeServiceWorkerPool.stop();
});
group('getDeviceCapabilities', () {
test(
'returns an List<DeviceCapabilities> if the http call completes successfully',
() async {
final mockHttpClient = MockHttpClientBuilder().generateClient();
final mockedFutureSDRService =
FutureSDRService.create(mockHttpClient, converter);
final response = await rootBundle
.loadString('assets/network/device_capabilities.json', cache: false);
final responseJson = json.decode(response);
final parsed = List<DeviceCapabilities>.empty(growable: true);
for (var entry in responseJson) {
parsed.add(DeviceCapabilities.fromJson(entry));
}
final result = await mockedFutureSDRService.getDeviceCapabilities();
expect(result.body, parsed);
//verify(() => mockedFutureSDRService.getDeviceCapabilities()).called(1);
//expect(result.body, isA<List<DeviceCapabilities>>());
});
test('throws an exception if the http call completes with an error',
() async {
final mockHttpClient =
MockHttpClientBuilder().generateErrorClient('Server Error', 500);
final mockedFutureSDRService =
FutureSDRService.create(mockHttpClient, converter);
final result = await mockedFutureSDRService.getDeviceCapabilities();
expect(result.statusCode, 500);
});
});
}
I like my solution so far, because i can exchange the httpClient easily. The only thing i would like to improve is to have the setup and shutdown of the converter directly integrated in the ChopperService, but i didnt find a proper way of doingthat so far.
Thanks for your help so far and I wish you a happy new year!
@ratzrattillo I'm not sure why you need to test decoding your models from JSON with a mocked Chopper client. What's the purpose of doing that? The Chopper functionality has already been tested by us package maintainers. You should simply feed that raw JSON directly to your model's fromJson
method if you use json_serializable
for example.
@techouse Yes, actually you are right. I do not seem to need that. The information regarding the custom converter was nonetheless definitely helpful to me. Thank you for your help. I will close this issue as it arose from a lack of understanding on my side.
Is your feature request related to a problem? Please describe. I do not know how to properly do unit testing with Chopper My Problem is described detailed in https://github.com/lejard-h/chopper/discussions/539
Describe the solution you'd like I would like to have extended documentation on how to properly Mock Chopper Clients and do Unit tests with Chopper.
Describe alternatives you've considered None
Additional context https://github.com/lejard-h/chopper/discussions/539