How to mock Socket.io client in Flutter tests
Asked Answered
A

2

9

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! πŸ™

Anglice answered 1/7, 2021 at 18:25 Comment(3)
Have you got any further? facing the same problem – Sliest
Unfortunately no. For now, we have written the tests using a mocked socket client using mocktail. We provide that mocked socket client into the widget when running the tests. – Anglice
mock your SocketClient ? – Aksoyn
C
2

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.

Counselor answered 15/6, 2023 at 8:52 Comment(0)
K
1

You can use await IOOverrides for override standard Socket method like socketConnect

Exemple:

await IOOverrides.runZoned(() async {
  your code 
}, socketConnect: (host, port, {sourceAddress, int sourcePort = 0, timeout}) => Future.value(mockSocket));
Koerlin answered 1/11, 2022 at 12:4 Comment(1)
Sorry, but socketConnect returns an instance of a Socket class which is not interchangeable with an instance of a Socket class from socket.io – Anglice

© 2022 - 2024 β€” McMap. All rights reserved.