Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock Socket.io client in Flutter tests

I'm using socket_io_client v0.9.12 in my flutter app (still not using null safety). I am creating a socket connection with my back-end server when the app starts and allow child widgets to access it using a provider.

I am trying to start creating the connection in the constructor so that when I need to use the socket client, it would probably be initialized already.

This is my provider class which has the creation of the socket connection:

class SocketClient {
  SocketClient() {
    _initializer = initialize();
  }

  Future _initializer;

  /// Socket client
  Socket client;

  /// Wait until the client is properly connected with the server
  Future finishInitializing() => _initializer;

  /// Initialize the socket connection with the server
  Future initialize() async {
    final completer = Completer<Socket>();

    final socket = io(
      'http://localhost:3000/gateway',
      OptionBuilder() //
          .setTransports(['websocket'])
          .disableAutoConnect()
          .setQuery({'token': 'TOKEN'})
          .build(),
    )..connect();

    socket
      ..onConnect((_) {
        completer.complete(socket);
      });

    client = await completer.future;
  }
}

The main app widget I have:

@override
Widget build(BuildContext context) {
  return Provider<SocketClient>(
    create: (_) => SocketClient(),
    builder: (context, _) {
      return Scaffold(
          ...
      );
    },
  );
}

This is how I am trying to use the socket client:

Future<void> foo(SocketClient socketClient) async {
  await socketClient.finishInitializing();
  final socket = socketClient.client;
  socket.on(eventName, (dynamic uploadDrawingEvent) {
    ...
  });
}

Everything works fine up to now. šŸŽ‰

However, when I try to run my tests that depend on this socket client, I see the below error:

Pending timers:
Timer (duration: 0:00:20.000000, periodic: false), created:
#0      new FakeTimer._ (package:fake_async/fake_async.dart:284:41)
#1      FakeAsync._createTimer (package:fake_async/fake_async.dart:248:27)
#2      FakeAsync.run.<anonymous closure> (package:fake_async/fake_async.dart:181:19)
#5      Manager.connect (package:socket_io_client/src/manager.dart:232:19)
#6      Manager.open (package:socket_io_client/src/manager.dart:194:41)
#7      Socket.connect (package:socket_io_client/src/socket.dart:104:8)
#8      SocketClient.initialize (package:flutter_app/web/services/socket_provider.dart:48:8)
<asynchronous suspension>
(elided 2 frames from dart:async)

══╔ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ā•žā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
The following assertion was thrown running a test:
A Timer is still pending even after the widget tree was disposed.
'package:flutter_test/src/binding.dart':
Failed assertion: line 1241 pos 12: '!timersPending'

When the exception was thrown, this was the stack:
#2      AutomatedTestWidgetsFlutterBinding._verifyInvariants (package:flutter_test/src/binding.dart:1241:12)
#3      TestWidgetsFlutterBinding._runTestBody (package:flutter_test/src/binding.dart:803:7)
<asynchronous suspension>
(elided 2 frames from class _AssertionError)
...

initialize function keeps hanging after client = await completer.future; line. That future is not resolved even after the test is finished (see the error).

I would like to get ideas on how I can mock this socket client properly to avoid this error.

I'm looking more towards an answer which mocks the functionality of the socket client because I have some more tests to write to cover the event handlers.

Thank you in advance! šŸ™

like image 913
Eranga Heshan Avatar asked May 29 '26 18:05

Eranga Heshan


1 Answers

Looking around in the code of socket_io_client, I saw that when a Socket is created, it creates a Transports.newInstance, which in the case of the test, uses the io implementation in io_transports.dart and returns an IOWebSocketTransport. It then creates a WebSocket.connect from dart:http which uses in the end HttpClient from dart:http.

HttpClient can be mocked tests using HttpOverrides.runZoned.


So ultimately, you should be able to mock your socket doing:

// An implementation of the HttpClient interface
class MyHttpClient implements HttpClient {
  MyHttpClient(SecurityContext? c);

  @override
  noSuchMethod(Invocation invocation) {
    // your implementation here
  }
}

void main() {
  HttpOverrides.runZoned(() {
    // Operations will use MyHttpClient instead of the real HttpClient
    // implementation whenever HttpClient is used.
  }, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
}

I created an issue in the repo here to ask for some guidance on how it is possible to do that.

like image 98
Valentin Vignal Avatar answered Jun 01 '26 07:06

Valentin Vignal