Flutter Widget testing with HttpClient

Issue

I am trying to write a widget test for a screen, not the main app. It’s my first time writing a widget test and I couldn’t find a proper solution for the issue.
I don’t know how to write a proper test for this one. I tried to write a simple widget test and it end up giving me an error as below
"Warning: At least one test in this suite creates an HttpClient. When
running a test suite that uses TestWidgetsFlutterBinding, all HTTP
requests will return status code 400, and no network request will
actually be made. Any test expecting a real network connection and
status code will fail.
To test code that needs an HttpClient, provide your own HttpClient
implementation to the code under test, so that your test can
consistently provide a testable response to the code under test."
I have just started learning it please help me.
NOTE: my test was just writing a basic test for finding Text widgets.

class BookingDetails extends StatefulWidget {
final booking;
BookingDetails(this.booking);
@override
_BookingDetailsState createState() => _BookingDetailsState();
}

class _BookingDetailsState extends State<BookingDetails>
with AutomaticKeepAliveClientMixin {

Row _buildTeacherInfo(Map<String, dynamic> teacherData) {
return teacherData != null
    ? Row(
        children: <Widget>[
          CircleAvatar(
            radius: 53,
            backgroundColor: MyColors.primary,
            child: CircleAvatar(
              radius: 50.0,
              backgroundImage: teacherData['user']['img_url'] == null ||
                      teacherData['user']['img_url'] == ''
                  ? AssetImage('assets/images/placeholder_avatar.png')
                  : NetworkImage(teacherData['user']['img_url']),
              backgroundColor: Colors.transparent,
            ),
          ),
          SizedBox(width: 20.0),
          Column(
            children: <Widget>[
              Container(
                child: Column(
                  children: <Widget>[
                    Text(
                      '${teacherData['user']['first_name']} ',
                      style: AppStyles.textHeader1Style,
                    ),
                    Text(
                      '${teacherData['user']['last_name']}',
                      style: AppStyles.textHeader1Style,
                    ),
                  ],
                ),
              ),
              ElevatedButton(
                onPressed: () {
                  //View Profile method
                },
                style: ElevatedButton.styleFrom(
                  primary: MyColors.primary,
                  shape: const RoundedRectangleBorder(
                      borderRadius: BorderRadius.all(Radius.circular(25))),
                ),
                child: Row(
                  mainAxisAlignment: MainAxisAlignment.center,
                  mainAxisSize: MainAxisSize.min,
                  children: <Widget>[
                    Icon(Icons.next_plan_outlined),
                    SizedBox(width: 10.0),
                    Text('VIEW PROFILE'),
                  ],
                ),
              ),
            ],
          ),
        ],
      )
    : Row(
        children: <Widget>[
          CircleAvatar(
            radius: 48,
            backgroundColor: MyColors.primary,
            child: CircleAvatar(
              radius: 45.0,
              backgroundImage:
                  AssetImage('assets/images/placeholder_avatar.png'),
              backgroundColor: Colors.transparent,
            ),
          ),
          SizedBox(width: 20.0),
          Expanded(
            child: Text(
              'Teacher allocation in progress',
              style: AppStyles.textHeader1Style,
            ),
          )
        ],
      );
  }

Widget _buildBookingDetails(
Map<String, dynamic> booking,
List<dynamic> campusData, // one campus' data is an array for some reason.
Map<String, dynamic> instData,
) {
return Expanded(
  child: Scrollbar(
    child: ListView(
      children: [
        ListTile(
          leading: Icon(Icons.location_on),
          title: Text(
            '${campusData[0]['address_line1']},'
            ' ${campusData[0]['suburb']}, '
            '${campusData[0]['state']} ${campusData[0]['postcode']} ',
            style: AppStyles.textHeader3Style,
          ),
        ),
}

@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
  future: Future.wait([_teacherData, _campusData, _classData, _instData]),
  builder: (context, snapshot) => snapshot.connectionState ==
          ConnectionState.waiting
      ? MyLoadingScreen(message: 'Loading booking data, please wait...')
      : snapshot.hasData
          ? SafeArea(
              child: Container(
                margin: const EdgeInsets.only(top: 30.0),
                child: Padding(
                  padding: const EdgeInsets.all(30),
                  child: Column(
                    mainAxisAlignment: MainAxisAlignment.start,
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: <Widget>[
                      _buildTeacherInfo(snapshot.data[0]),
                      Divider(color: MyColors.dividerColor),
                      SizedBox(height: 10),

                      const SizedBox(height: 10),
                      Divider(
                        color: MyColors.primary,
                        thickness: 1,
                      ),
                      const SizedBox(height: 10),
                      _buildBookingDetails(
                        widget.booking,
                        snapshot.data[1],
                        snapshot.data[3],
                      ),
                      SizedBox(height: 10),
                      Divider(
                        color: MyColors.primary,
                        thickness: 1,
                      ),
                      SizedBox(height: 10),
                      Center(
                        child: widget.booking['cancelled_by_inst'] == true
                            ? Text(
                                'Canceled',
                                style: AppStyles.textHeader3StyleBold,
                              )
                            : widget.booking['teacher_id'] == null
                                ? Center(
                                    child: Text(
                                      'Teacher Allocation in Progress',
                                      style: AppStyles.textHeader3StyleBold,
                                    ),
                                  )
                                : null,
                      ),
                     }

Solution

I have reduced your code to the following minimal version, to be able to execute it:

snippet.dart:

import 'package:flutter/material.dart';
import 'dart:convert';
import 'api.dart';

class BookingDetails extends StatefulWidget {
  final Map<String, String> booking;
  BookingDetails(this.booking);
  @override
  _BookingDetailsState createState() => _BookingDetailsState();
}

class _BookingDetailsState extends State<BookingDetails> {
  late Future _campusData;

  Future<dynamic> _fetchCampusData() async {
    var campusID = widget.booking['campus_id'];
    if (campusID != null) {
      var response = await api.getCampusByID(campusID);
      return json.decode(response.body);
    }
  }

  @override
  void initState() {
    _campusData = _fetchCampusData();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
        future: _campusData,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            return const Text('Displaying data');
          } else if (snapshot.hasError) {
            return const Text('An error occurred.');
          } else {
            return const Text('Loading...');
          }
        }

    );
  }
}

api.dart:

import 'package:http/http.dart' as http;

final _ApiClient api = _ApiClient();

class _ApiClient {
  Future<http.Response> getCampusByID(String id) async {
    var url = Uri.parse('https://run.mocky.io/v3/49c23ebc-c107-4dae-b1c6-5d325b8f8b58');
    var response = await http.get(url);
    if (response.statusCode >= 400) {
      throw "An error occurred";
    }
    return response;
  }
}

Here is a widget test which reproduces the error which you described:

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:widget_test/snippet.dart';

void main() {

  testWidgets('Should test widget with http call', (WidgetTester tester) async {
    var booking = <String, String>{
      'campus_id': '2f4fccd2-e199-4989-bad3-d8c48e66a15e'
    };

    await tester.pumpWidget(TestApp(BookingDetails(booking)));
    expect(find.text('Loading...'), findsOneWidget);

    await tester.pump();
    expect(find.text('Displaying data'), findsOneWidget);
  });
}

class TestApp extends StatelessWidget {
  final Widget child;

  TestApp(this.child);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: child,
    );
  }
}

Here is the error message, for a reference:

Test failed. See exception logs above.
The test description was: Should test widget with http call

Warning: At least one test in this suite creates an HttpClient. When
running a test suite that uses TestWidgetsFlutterBinding, all HTTP
requests will return status code 400, and no network request will
actually be made. Any test expecting a real network connection and
status code will fail.
To test code that needs an HttpClient, provide your own HttpClient
implementation to the code under test, so that your test can
consistently provide a testable response to the code under test.

Solution

The error tells you what the problem is: you must not execute HTTP calls in the widget tests. So you need to mock that HTTP call out, so that the mock is called instead of the real HTTP call. There are many options with which you can do that, e.g. using the mockito package.

Here a possible solution using the nock package which simulates an HTTP response at the HTTP level.

pubspec.yaml:

dev_dependencies:
  nock: ^1.1.2

Widget test:

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:nock/nock.dart';
import 'package:widget_test/snippet.dart';

void main() {
  setUpAll(nock.init);

  setUp(() {
    nock.cleanAll();
  });

  testWidgets('Should test widget with http call', (WidgetTester tester) async {
    nock('https://run.mocky.io')
        .get('/v3/49c23ebc-c107-4dae-b1c6-5d325b8f8b58')
      .reply(200, json.encode('{"id": "49c23ebc-c107-4dae-b1c6-5d325b8f8b58", "name": "Example campus" }'));

    var booking = <String, String>{
      'campus_id': '2f4fccd2-e199-4989-bad3-d8c48e66a15e'
    };

    await tester.pumpWidget(TestApp(BookingDetails(booking)));
    expect(find.text('Loading...'), findsOneWidget);

    await tester.pump();
    expect(find.text('Displaying data'), findsOneWidget);
  });
}

class TestApp extends StatelessWidget {
  final Widget child;

  TestApp(this.child);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: child,
    );
  }
}

Answered By – Janux

Answer Checked By – Senaida (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *