Mocking http.Client (updated)
In the previous post from 2022, we already saw that we can use Mockito with @GenerateNiceMocks to generate a MockClient, or use http's own MockClient and pass it to classes. But what if the code is from a package that does not accept a Client?
Investigation
In that case we can't easily pass a mock Client. Let's look at the code that will run with the test:
var response = await http.get(Uri.parse(url), headers: allHeaders);If we dig into http's code, we see:
Future<Response> get(Uri url, {Map<String, String>? headers}) =>
_withClient((client) => client.get(url, headers: headers));
Future<T> _withClient<T>(Future<T> Function(Client) fn) async {
var client = Client();
try {
return await fn(client);
} finally {
client.close();
}
}
abstract interface class Client {
/// Creates a new platform appropriate client.
///
/// Creates an `IOClient` if `dart:io` is available and a `BrowserClient` if
/// `dart:js_interop` is available, otherwise it will throw an unsupported
/// error.
factory Client() => zoneClient ?? createClient();
...
}
@internal
Client? get zoneClient {
final client = Zone.current[#_clientToken];
return client == null ? null : (client as Client Function())();
}
/// Runs [body] in its own [Zone] with the [Client] returned by [clientFactory]
/// set as the default [Client].
/// [...]
/// > [!IMPORTANT]
/// > Flutter does not guarantee that callbacks are executed in a particular
/// > [Zone].
/// >
/// > Instead of using [runWithClient], Flutter developers can use a framework,
/// > such as [`package:provider`](https://pub.dev/packages/provider), to make
/// > a [Client] available throughout their applications.
/// >
/// > See the
/// > [Flutter Http Example](https://github.com/dart-lang/http/tree/master/pkgs/flutter_http_example).
R runWithClient<R>(R Function() body, Client Function() clientFactory,
{ZoneSpecification? zoneSpecification}) =>
runZoned(body,
zoneValues: {#_clientToken: Zone.current.bindCallback(clientFactory)},
zoneSpecification: zoneSpecification);
runWithClient
Here's how I used runWithClient to pass my MockClient:
await http.runWithClient(
() async {
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Provider<FirebaseAuth>(
create: (_) => MockFirebaseAuth(),
child: Provider<ChatService>(
create: (_) => MockChatService(),
child: BubbleWidget(b),
),
),
),
),
);
},
() => MockClient((request) async {
final content = File('test/airtag.html').readAsStringSync();
return http.Response(
content,
200,
headers: {'content-type': 'text/html'},
);
}),
);
content-type
I can see that my MockClient callback is executed but I get this exception when initializing Response:
[package_name] - Error in https://www.[site].com/[some_article].html response (Invalid argument (string): Contains invalid characters.: "<!DOCTYPE html><html lang=\"en-US\"><head><meta charSet=\"utf-8\"/>I can see that readAsStringSync() reads in utf-8 by default, so content is fine. Should I add content-type to the headers?
I tried with headers: {'content-type': 'text/html', 'charset': 'utf-8'}, but it failed too.
Later when I googled some more, I found a post that set it up like this: headers: {'content-type': 'text/html; charset=utf-8'}. And sure enough, this worked.
Looking back, if I had carefully read the documentation, I'd have found out myself:
/// An HTTP response where the entire response body is known in advance.
class Response extends BaseResponse {
/// The bytes comprising the body of this response.
final Uint8List bodyBytes;
/// The body of the response as a string.
///
/// This is converted from [bodyBytes] using the `charset` parameter of the
/// `Content-Type` header field, if available. If it's unavailable or if the
/// encoding name is unknown:
/// - [utf8] is used when the content-type is 'application/json' (see [RFC 3629][]).
/// - [latin1] is used in all other cases (see [RFC 2616][])
///
/// [RFC 3629]: https://www.rfc-editor.org/rfc/rfc3629.
/// [RFC 2616]: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html
String get body => _encodingForHeaders(headers).decode(bodyBytes);
/// Creates a new HTTP response with a string body.
Response(String body, int statusCode,
Here's the examples from https://www.rfc-editor.org/rfc/rfc9110.html (redirected from RFC 2616):
For example, the following media types are equivalent in describing HTML text data encoded in the UTF-8 character encoding scheme, but the first is preferred for consistency (the "charset" parameter value is defined as being case-insensitive in [RFC2046], Section 4.1.2):
text/html;charset=utf-8
Text/HTML;Charset="utf-8"
text/html; charset="utf-8"
text/html;charset=UTF-8Now the test works fine.
Clean up with Provider?
According to the docs for runWithClient, in Flutter, we can use a Provider instead. This makes the code much cleaner:
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Provider<http.Client>(
create: (_) => MockClient((request) async {
MockClient((request) async {
final content = File('test/airtag.html').readAsStringSync();
return http.Response(
content,
200,
headers: {'content-type': 'text/html; charset=utf-8'},
);
}),
}),
child: BubbleWidget(b),
),
),
),
);
Unfortunately, this did not work. I don't know the system well enough to figure out why. But basically, the Widget that was supposed to use Client and render, seemed to be immediately destroyed?... I don't even know if that's possible. I put breakpoints in that Widget's build and it was never called.
So I ended up using runWithClient. Maybe some day I'll figure out how to use Provider.