Search code examples
node.jsudptimeoutmocha.js

How to close listening UDP sockets when mocha times-out?


I have some tests which bind to a UDP port, e.g.

describe('the server', function() {
  it('should respond to a udp packet', function(done) {
    const udp = dgram.createSocket('udp4');

    udp.on('error', function(error) {
      console.log(error);
    });

    udp.on('message', function(msg, rinfo) {
      console.log('msg', msg);
      console.log('rinfo', rinfo);
      // TODO: check the reply
      udp.close();
      done();
    });
    udp.bind();

    // send a request
    const bytes = Buffer.from("421a0800117860bc457f0100001a0653455256455222054d54303031", 'hex');
    udp.send(bytes, SERVER_PORT, SERVER_ADDR)
  });
});

When the test completes successfully, that code path can call udp.close() which lets mocha exit.

When a test exceeds mocha's 2 second time-out, I see the error but mocha does not exit as the UDP port is still listening.

Is there some form of callback/event that I can use to close a listening UDP socket when mocha's 2 second time-out fires?


Solution

  • This can be achieved with an afterEach hook. An afterEach hook is a function that gets executed after each test, whether passed or failed (but not skipped). So it is useful to perform cleanup operations, like closing a socket in this case.

    Like all Mocha hooks, an afterEach is defined inside a describe function call, and it applies to all tests in that scope. If you only want the hook to run after one particular test only, put that test into a dedicated describe function.

    So the changes to do here are the following:

    • Move the declaration of udp from the it function into the containing describe function. This will make udp accesible from both it and afterEach.
    • Add an afterEach hook to perform the cleanup operation: udp.close();. Remember that this will run whether the test passes or fails for any reason, so it's a good idea to avoid assumptions about the progress of the test. Here: don't assume that udp has been already created.
    • Remove other calls to udp.close();, these are no longer needed now.
    describe('the server', function() {
      let udp;
    
      afterEach(() => {
        if (udp) {
          udp.close();
          udp = undefined;
        }
      });
    
      it('should respond to a udp packet', function(done) {
        udp = dgram.createSocket('udp4');
    
        udp.on('error', function(error) {
          console.log(error);
        });
    
        udp.on('message', function(msg, rinfo) {
          console.log('msg', msg);
          console.log('rinfo', rinfo);
          // TODO: check the reply
          done();
        });
        udp.bind();
    
        // send a request
        const bytes = Buffer.from("421a0800117860bc457f0100001a0653455256455222054d54303031", 'hex');
        udp.send(bytes, SERVER_PORT, SERVER_ADDR)
      });
    });
    

    One thing to note is that in case of a timeout, the test function may keep running after the socket has been closed, producing all kinds of baffling error messages: this is not surprising.

    Another interesting thing to note is that variables scoped in a describe function call (unlike those in an it function call) have the same lifetime of the Mocha run. For this reason it is a common practice to create those variable lazily when needed (in the tests) and to clear up manually by setting them explicitly to undefined or null as soon as they are no longer needed.