To test a module that uses socket.io
we need to intercept a socket.io
connection and mock a server responses. If a module tries to establish a socket.io
connection immediately on a module evaluation then it won't reach the mocked socket.io
.
To overcome the problem we either have to delay the evaluation of the modules we test, or delay establishing the socket.io
connection.
The steal-socket package addresses the problem using the second option.
To test or demo a functionality that uses socket.io
we need to mock a socket.io connection and emulate a socket.io server.
The following package can-fixture-socket intercepts socket.io messages and allows to simulate server responses.
// Import socket-io client:
var io = require("socket.io-client");
// Import fixture module:
var fixtureSocket = require("can-fixture-socket");
// Create a mock server that intercepts socket.io:
var mockServer = new fixtureSocket.Server(io);
// Mock server behavior
mockServer.on("connection", function(){
mockServer.emit("notifications", {test: "OK"})
});
To intercept socket.io
messages can-fixture-socket
overrides io.Manager
prototype methods (e.g. open
,socket
), returns a mocked io.Socket
socket, and uses internal pubsub to connect client's socket event listeners with the mocked server.
For this to work new fixtureSocket.Server(io)
must happen before any call to io()
is done.
If any imported module of a demo page or a test calls to io()
immediately then there is no connection between
a client and the mocked server.
A demo page:
<script type="text/stache" can-autorender id='main'>
NOTE: The following module imports a model that executes `io()` immediately.
<can-import from="bitcentive/components/contribution-month/" />
<contribution-month></contribution-month>
</script>
<script src="../../../node_modules/steal/steal.js"
main="can-view-autorender">
// Use fixtures:
import io from "socket.io-client/socket.io";
import "can-fixture-socket";
// NOTE: the following line gets executed after the above `ContributionMonth` model calls `io()`.
var mockServer = new fixtureSocket.Server( io );
// mock server responses here ...
</script>
A test:
// import socket fixtures:
import 'bitcentive/models/fixtures/fixtures-socket';
// import module that we want to test:
import { ViewModel } from 'bitcentive/components/contribution-month/';
QUnit.asyncTest('test fixture socket', function(){
let vm = new ViewModel();
vm.contributionMonthPromise.then((contributionMonth) => {
QUnit.equal(contributionMonth.items.length, 3, 'Loaded 3 items');
QUnit.start();
});
});
Both demo page and test won't work because the initial call to io
will not connect to the mocked server.
One way to pass around the problem is to use dynamic import of the modules that we are testing / demoing.
Example demo page that has the problem can be fixed with the following modifications:
<script type="text/stache" can-autorender id='main'>
The following module imports a model that executes `io()` immediately.
<can-import from="bitcentive/components/contribution-month/" />
<contribution-month></contribution-month>
</script>
<script src="../../../node_modules/steal/steal.js"
main="@empty"> // <<<<<< NOTE
import io from "socket.io-client/socket.io";
import "can-fixture-socket";
var mockServer = new fixtureSocket.Server( io );
// mock server responses here ...
// NOTE: Postpone autorender till socket.io gets mocked:
// This way the module that we want to test will be evaluated after we mock socket.io.
System.import('can-view-autorender').then(function(autorender){ // <<<<<< NOTE
autorender(function(){
viewModel(document.getElementById("main")).set("contributionMonthId","1");
});
});
</script>
Example test can work by postponing the import of the module:
// import socket fixtures:
import 'bitcentive/models/fixtures/fixtures-socket';
// Delayed import of the module that we want to test:
var VMPromise = System.import('bitcentive/components/contribution-month/').then(module => module.ViewModel);
QUnit.asyncTest('test fixture socket', function(){
// Wait till our module gets loaded:
VMPromise.then(ViewModel => {
let vm = new ViewModel();
vm.contributionMonthPromise.then((contributionMonth) => {
QUnit.equal(contributionMonth.items.length, 3, 'Loaded 3 items');
QUnit.start();
});
});
});
If we can delay socket.io connection (and all calls that create socket event listeners) till we mock a socket.io
server we can demo / test as we usually do (see demo and test as described in the "Problem" section).
- Use
steal-socket.io
instead ofsocket.io-client
- The
steal-socket.io
package exposes a wrappedio
function. - It records all calls to
io()
and tosocket.on(...)
(socket.emit(...)
, etc) to a FIFO storage. - On
steal.done()
it replays the recorded calls applying them to realio
socket and then continue as a proxy.
// The following app:
var socket = io("localhost");
socket.on("messages", function(m){ console.log(m); })
socket.emit("hello", {});
// will create:
fifoSockets = {
"localhost": {
url: "localhost",
realSocket: null,
fifo: [
[io, ["localhost"]],
["on", ["messages", function(m){ console.log(m); }]],
["emit", ["hello", {}]]
]
}
}
The package exposes a funcion that will return a delayed socket
instead of a regular io
socket. Which is mocked socket with methods like on/off/emit/once.
And then on steal.done()
the package will replay the recorded calls applying them to real io
:
steal.done().then(() => {
var fifo = fifoSockets.localhost.fifo
io = fifo[0][0],
ioArgs = fifo[0][1];
// Replay call to io:
var realSocket = io.apply(null, ioArgs);
// Replay calls to io socket:
fifo.forEach( [method, args] => {
realSocket[method].apply(realSocket, args);
}
})