6
\$\begingroup\$

I am working on creating a test library, and I found DI to be pretty convenient, maybe even to create more functional coding patterns.

Normally we can have an awkward before-hook setup in Node.js testing libraries, and it's always a little unclear when things actually get initialized and refactoring can be harder than it should be:

let client;

before(function(){
    return client = ...     // client requires asynchronous initalization
});

it('tests client', function(){
   assert(client.isConnected);   // client is declared / initialized...right?
});

I've always hated that pattern, sometimes just for ergonomic reasons. It just seems awkward to me. Because client will be in scope in places where it is not yet initialized.

I thought about avoiding this pattern and finally devised a way around it, it basically looks like this:

describe('parent', function(){

    inject(() => {
        return {
            client: new Client({port: 7033}).ensure()  // returns a promise
        }
    });

    describe('child',  (client) => {

       // client has been initialized, and is injected here

        it('tests client', function(){
          // ok we know for sure client is defined
          assert(client.isConnected);
        });

    });

});

as you can see in any child block, we can inject parameters from the parent block.

This is simply a nicety, and makes things easier to grok and less error prone. Maybe even more functional programming style, not sure.

My real concern is supporting static typing, with TypeScript and allowing for at least some static analysis for correctness.

My question is - does anyone know if there is some ES7 or ES8 feature (maybe annotations/decorators?) that can somehow allow for some name checking or type checking in this kind of scenario?

I don't want to create some crazy feature that is so dynamic that it's hard to use.

Basically the way it works currently is that Promise.resolve is called for key/value returned from the inject hook, then this value is injected into the child blocks, by name. So the parameter to the child block callback function will have to match the key of the returned object from the inject hook. This is an API feature in progress and looking for constructive criticism.

To clarify, this would be an alternative API design option:

   inject('client', () => {
        return new Client({port: 7033}).ensure()
    });

then we could use the string 'client' as the matching value to determine where to inject the client variable. Of course, you would only be able to return a single value from this, and not a tuple.

I am also looking for a good way to implement this with callbacks instead of promises. I am not going to reinvent the wheel, so somewhere along the way we should use the async library or similar, here is what I think is the best option:

   inject.cb(j => {
        async.parallel({a: foo(), b: bar()}, j);
    });


    describe('child',  (a, b) => {

       // a and b have been initialized, and are injected here

    });

this way my library could work with any other library that fired a final callback with this signature:

function complete(err, results){}

where results is a simple key-value map; j would be the library callback which accepts the above arguments.

Another issue of course, is handling immutability, because the same object(s) will get injected into multiple places:

    inject('client', () => {
        return new Client({port: 7033}).ensure()
    });


    describe('child1',  (client) => {

       // client is the same object as the below child block

    });

    describe('child2',  (client) => {

       // client is the same object as the above child block

    });

So should make client immutable somehow.

\$\endgroup\$
6
  • \$\begingroup\$ Do you have a callback when client is connected? This sounds like a case where the asynchronous done would be useful. before(function (done) { client.connect(function () { done(); }); } \$\endgroup\$
    – user650881
    Jan 29, 2017 at 10:37
  • \$\begingroup\$ right, it uses promises in this case, not a callback to ensure that client has been asynchronously initialized. (The ensure() method returns a promise in the outline.) I should also support callbacks somehow. \$\endgroup\$ Jan 29, 2017 at 21:46
  • \$\begingroup\$ but your question is all the more reason why static analysis/typing would be a nicety here, and I want to support that! :) \$\endgroup\$ Jan 29, 2017 at 21:47
  • 1
    \$\begingroup\$ Same thing in that case. Assuming it returns the promise, call then on the promise and pass the asynchronous completion done. Would that address the problem? \$\endgroup\$
    – user650881
    Jan 29, 2017 at 23:52
  • \$\begingroup\$ Did you read the help center? It looks like you've stripped your code beyond recognition. Now it's hard to say whether new suggestions will actually fit your codebase. \$\endgroup\$
    – Mast
    Feb 2, 2017 at 9:42

1 Answer 1

1
\$\begingroup\$

As far as I can tell, the use case you are targeting is already supported by the completion callback, even when you are using promises. I have had good luck using the following pattern to ensure that asynchronously initialized, promise based libraries are fully initialized before testing begins:

let client;

// Note the function now accepts argument `done`
before(function(done){
    client = new Client({port: 7033}).ensure().then(done)
                                     .catch(function (reason) {
                                         console.error(reason);
                                     });
});

it('tests client', function(){
    assert(client.isConnected);   // we know client is initialized
});

As you correctly pointed out this is similar to returning the promise.

let client;

before(function(){
    client = new Client({port: 7033}).ensure()
                                     .catch(function (reason) {
                                         console.error(reason);
                                         throw reason;
                                     });
    return client;
});

it('tests client', function(){
    assert(client.isConnected);   // we know client is initialized
});

In either case, the initialization should be guaranteed complete before testing continues. So unless I am missing something, there should not be any ambiguity about the initialization state.

If you wish to initialize before each test, it works just as well with beforeEach(...). Also, note that there is a timeout that will cause test failure if done is not called quickly enough. Reference the docs if you need to extend the timeout as there are a variety of patterns.

While I think the existing pattern fully addresses your use case, you might prefer the argument injection for stylistic reasons. In that case I propose you can create a wrapper that calls the test function in the context of the client's then handler.

function provide_client(testFunc) {
    return function (done) {
        // Here I assume that the initialized client is passed through `then`.
        // If not fetch it from where ever appropriate.
        new Client({port: 7033}).ensure().then(function (client) {
            testFunc(client);
            done();
        }).catch(function (reason) {
            console.error(reason);
        });
    };
}

// Application
it('tests client', provide_client(function (client) {
    assert(client.isConnected);
}));
\$\endgroup\$
2
  • \$\begingroup\$ you don't have to pass the done callback, you can just return the client object from the before hook, because you are returning a promise. also, this is the actual problem. Because client is in scope outside of the blocks, which is what we want to avoid. \$\endgroup\$ Feb 5, 2017 at 2:02
  • \$\begingroup\$ I was not aware returned promises now await async completion in place of calling then. However this leaves me confused by your assertion that client will be in scope in places where it is not yet initialized. I believe the async completion means that it is initialized successfully before test, otherwise the test suite will fail and terminate. If separate instance is needed for each test beforeEach (and afterEach) can ensure each test gets a freshly created instance. Still if you are stylistically set on injection try a decorator with done and injected arg invoked in client.then? \$\endgroup\$
    – user650881
    Feb 5, 2017 at 11:56

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.