Flutter Widget testing with HttpClient

3,029

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,
    );
  }
}
Share:
3,029
Gurjit Singh
Author by

Gurjit Singh

Updated on January 01, 2023

Comments

  • Gurjit Singh
    Gurjit Singh over 1 year

    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,
                          ),
                         }
    
    • Janux
      Janux over 2 years
      When you post a snippet, it would be helpful if it is a minimal representation of the problem, i.e. leaving out each and everything which is not relevant to the problem.
    • Gurjit Singh
      Gurjit Singh over 2 years
      Tried to do my best to give an insight into the screen.
  • Gurjit Singh
    Gurjit Singh over 2 years
    Thanks, Janux, it helped a lot to learn but one question why does it sometimes gives me an error about the widget tree when I want to test a particular one?
  • Janux
    Janux over 2 years
    Without seeing the details of that error, I can't tell. However, I supposes that this problem is not related to the question how to create tests for widgets using HTTP calls. So, I suggest to you to do some further investigations and create a new question if necessary.
  • Gurjit Singh
    Gurjit Singh over 2 years
    I solved the issue thanks Janux.