0

我正在尝试为屏幕而不是主应用程序编写小部件测试。这是我第一次编写小部件测试,但我找不到合适的解决方案。我不知道如何为此编写一个适当的测试。我尝试编写一个简单的小部件测试,但最终给我一个错误,如下所示“警告:此套件中的至少一个测试创建了一个 HttpClient。当运行使用 TestWidgetsFlutterBinding 的测试套件时,所有 HTTP 请求都将返回状态代码 400,并且实际上不会发出网络请求。任何期望真实网络连接和状态代码的测试都将失败。要测试需要 HttpClient 的代码,请为被测代码提供您自己的 HttpClient 实现,以便您的测试可以始终如一地提供可测试的响应被测代码。” 我刚开始学习它,请帮助我。笔记:

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,
                      ),
                     }
4

1 回答 1

2

我已将您的代码减少到以下最小版本,以便能够执行它:

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;
  }
}

这是一个小部件测试,它重现了您描述的错误:

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,
    );
  }
}

以下是错误信息,供参考:

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.

解决方案

该错误告诉您问题所在:您不能在小部件测试中执行 HTTP 调用。所以你需要模拟那个 HTTP 调用,以便调用模拟而不是真正的 HTTP 调用。您可以使用许多选项来执行此操作,例如使用mockito包。

这是一个使用nock包的可能解决方案,它在 HTTP 级别模拟 HTTP 响应。

pubspec.yaml

dev_dependencies:
  nock: ^1.1.2

小部件测试:

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,
    );
  }
}
于 2021-09-20T05:47:13.683 回答