Para mockar um Method Channel, precisamos imitar o seu comportamento. Para conseguir isso podemos utilizar o TestDefaultBinaryMessenger.setMockMessageHandler.

O método setMockMessageHandle irá substituir o callback fornecido para o MethodChannel pelo handler especificado durante o teste. Dessa forma, temos controle sobre a dependência durante o contexto do teste.

Contexto

Imagine que você precisa realizar a migração parcial de um fluxo para Flutter, e nessa execução, você precisará passar dados da tela nativa para o Flutter. Como podemos garantir esse comportamento?

Nossa tela

import 'dart:convert';
 
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
 
void main() {
  runApp(const MyApp());
}
 
class MyApp extends StatelessWidget {
  const MyApp({super.key});
 
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const UserScreen(),
    );
  }
}
 
class UserScreen extends StatelessWidget {
  const UserScreen({super.key});
  static const MethodChannel _channel = MethodChannel('br.com.my_package/user_data_provider');
 
  Future<Map<String, dynamic>> _getUserData() async {
    try{
        final String result = await _channel.invokeMethod('getUserData');
        final Map<String, dynamic> data = jsonDecode(result);
 
        return data;
    } catch(e) {
        return {};
    }
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
            title: const Text('Teste'),
        ),
        body: FutureBuilder(
            future: _getUserData(),
            builder: (context, snapshot) {
                print('Oi $snapshot');
                if(snapshot.connectionState == ConnectionState.waiting) {
                    return const Center(child: CircularProgressIndicator());
                }
                else if(snapshot.hasError) {
                    return const Text('Erro!');
                }
                else if(!snapshot.hasData) {
                    return const Text('Não há dados!');
                }
                else {
                    final userData = snapshot.data!;
                    return Column(
                        children: [
                            Text('Nome: ${userData['name']}'),
                            Text('Idade: ${userData['age']}')
                        ],
                    );
                }
            },
        ),
    );
  }
}

Nosso teste

import 'package:app/main.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
 
void main() {
    group('Testando Method Channel', () {
        const stubbedMethodChannel = MethodChannel('br.com.my_package/user_data_provider');
 
        TestWidgetsFlutterBinding.ensureInitialized();
 
        setUp((){
            TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
                stubbedMethodChannel,
                (methodCall) async {
                    if(methodCall.method == 'getUserData') {
                        const userResponse = '''
                            {
                                "name": "Caique",
                                "age": 29
                            }
                        ''';
                        return userResponse;
                    }
                    return null;
                }
            );
        });
 
        testWidgets('Cenário 1 - Emissão básica', (tester) async {
            await tester.pumpWidget(const MyApp());
 
            expect(find.byType(UserScreen), findsOneWidget);
 
            await tester.pump(const Duration(seconds: 3));
 
            expect(find.text('Nome: Caique'), findsOneWidget);
            expect(find.text('Idade: 29'), findsOneWidget);
        });
    });
}

Conclusão

Em nosso exemplo, vimos como assumir o controle do MethodChannel durante o contexto do nosso teste. Para isso, utimizamos a função setMockMethodCallHandler, que permite substituirmos o callback do Channel pelo que gostaríamos.

Isso nos possibilita executar os cenários que nossa aplicação poderia ter sem mesmo termos de compilar o código nativo, o que torna o ciclo de feedback de desenvolvimento menor, possibilitando entregar com mais qualidade e em menos tempo.