Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Adding promise support to runnables (and thus tests). #329

Closed
wants to merge 1 commit into from
@domenic
  • If a runnable returns a duck-typed promise, i.e. an object with a then method, it gets treated as an async test, with success if the promise is fulfilled and failure if it is rejected (with the rejection reason as the error).
  • Includes tests both of new functionality and some to show that introducing this doesn't break any old functionality.

Would really appreciate merging this! I tried to stick as close as possible to the existing style. We <3 Mocha but also <3 promises.

@domenic domenic Adding promise support to runnables (and thus tests).
* If a runnable returns a duck-typed promise, i.e. an object with a `then` method, it gets treated as an async test, with success if the promise is fulfilled and failure if it is rejected (with the rejection reason as the error).
* Includes tests both of new functionality and some to show that introducing this doesn't break any old functionality.
7d1eeee
@tj
Owner
tj commented

sorry I dont want to abstract any of that stuff, just a callback is fine, .then() is not the only way to write promises etc so it would just be a mess and wouldn't support all the promise-style libs out there anyway

@tj tj closed this
@domenic

Well, all promise libs in widespread usage (Q, when, Windows 8 WinJS, jQuery, Dojo, ...) follow CommonJS Promises/A ("then-ables").

A callback forces me to write the following boilerplate every time:

promise.then(
    function () {
        (2 + 2).should.equal(4);
    },
    function (err) {
        throw new Error("Promise was rejected when it should not have been.");
    }
).then(done, done);

instead of, with this (tiny!) change,

return promise.then(function () {
    (2 + 2).should.equal(4);
});
@tj
Owner
tj commented

personally I dont really even get the point of promise libs, your API can defer things without it being a "promise", pretty much every node lib does this, but they dont use .then() etc

@domenic

I understand that you're not a fan, but enough people are that I was hoping you could accommodate them in your feature-rich library that makes asynchronous testing simple and fun. My goal is not to convince you to start using promises, but instead to make Mocha a good choice for those of us that already do (like, say, every jQuery, Dojo, or Windows 8 developer).

@tj
Owner
tj commented

i hate jquery's xhr api haha. we can let people vote here but -1 from me, ultimately mocha is about being simple and crud-free, adding support for adhoc things like this takes away some of the elegance IMO

@pbouzakis

+1

I use promises all the time and it has definitely helped keep our large codebase clean and readable.

@tj
Owner
tj commented

also for example lots of APIs support .end(callback) but I'm not adding support for returning those either, which are effectively the same thing as promises

@domenic

You already have added support for those, since they follow the style of conflating success and error callbacks, so you can just pass in done.

Promises are harder to work with without additional support from the testing framework, since they separate fulfilled and rejected cases in order to achieve exception-style error bubbling.

Such APIs are not really the same thing as promises in any way, since they do not support the various guarantees of a promise, or functionality such as chaining and bubbling.

@tj
Owner
tj commented

and that's an artifact of it being difficult to work with, that's not really something I want in mocha

@tj
Owner
tj commented

maybe we can come up with a way to make done() extensible, so people can have adhoc plugins for stuff like this

@domenic

I'd be up for that; I was just going to write a package that overrode mocha.Runnable.prototype.run, which of course would be a bitch to maintain in the face of upstream changes since that method is so long.

@kriskowal

+1, but you probably already know that

For what it’s worth, @visionmedia, there have been enough bad implementations of promises, including Node’s original Emitter promises and jQuery’s multi-promises, that skepticism toward the concept is well-deserved.

There has been enormous momentum toward supporting then as a way to freely-trade promises between various systems, a great tool for agreeing to disagree.

@kriskowal

Also, @visionmedia, for what it’s worth, promises are easy to work with. It’s just slightly inconvenient to have to switch between callbacks and promises, and that slight inconvenience goes both ways. That slight inconvenience becomes tedium without support at low levels. So we’re shopping around for a friendly test library.

@tj
Owner
tj commented

sure, that's understandable, I just prefer callbacks because they make less assumptions, and of course since every node lib in existence supports them. I'll think about it :)

@eldargab

@domenic and other promises funs. What about just extending your favorite promise libraries? Something like:

promise.step(function () {
    (2 + 2).should.equal(4);
}).cb(done);

definitely -1 from me, though it's not my concern

@kriskowal

@eldargab With any CommonJS/A compliant promise library; including Dojo, jQuery, and Q; you can do just that:

promise
.then(done, done);

This assumes that the promise fulfills to undefined, which is likely most of the time.

Our desire is to be able to do this instead, for the same effect:

return promise;

It’s not much, but it’s nice when using promises because returning the last promise is the common idiom.

@domenic

It also obviates the need for changing your test's signature to function ("foo", done) { from simply function ("foo") {, fitting with promises' general pattern of letting results flow through the system without needing to make sure everyone passes their callback responsibilities around correctly.

Finally, and this is the biggest one---perhaps I should have brought it up before---support for this will allow patterns like

return promise.should.be.rejected;
return promise.should.be.rejected(TypeError);
return promise.should.eventually.equal(5);

instead of

promise.should.be.rejected(done, done)
promise.should.be.rejected(TypeError, done, done);
promise.should.eventually.equal(5, done, done);

which I think we can agree is a really nice win for readability.

@tj
Owner
tj commented

returning in general as an API this sort of thing kinda sucks since you're then limited as far as assertions in callbacks go etc

@domenic

Well but it's not used in a callback system; the assertions would be transformations on the promises themselves.

@novemberborn

I use Mocha in my promise library and had to resort to making a test case factory that handles returned promise values. Relevant wrapper at https://github.com/promised-io/core/blob/master/test/test-case/index.js#L81.

@nonplus

+1 FWIW, Buster supports returning thenables from test cases and it looks like they'll be coming to Jasmine as well. I'm using @domenic's fork until this makes it into Mocha.

http://busterjs.org/docs/overview/

@tomjack

+1, currently using domenic's fork

@domenic

Rebased my fork's branch on top of master for anyone using it.

@jmreidy

+1. (Also, for anyone else, @domenic's fork is currently not supporting node 0.8.x, you may need to update your package.json file appropriately.)

@domenic

Thanks @jmreidy, rebased again.

@domenic

Announcing Mocha as Promised, for a more long-term solution. Let me know how it goes for you guys.

@mjackson

+1, would love to see support for this in Mocha!

@tj
Owner
tj commented

cool @domenic! should add those to the wiki

@mjackson

@domenic Was thinking about the approach you're taking in Mocha as Promised when it occurred to me that we could just wrap mocha's it function instead. Like this:

var q = require('q'); // or whatever

mocha_it = it;
it = function (desc, fn) {
  mocha_it(desc, function (done) {
    var doneCalled = false;
    var value = fn.call(this, function () {
      doneCalled = true;
      done.apply(this, arguments);
    });

    if (q.isPromise(value)) {
      if (doneCalled) {
        // error
      } else {
        value.then(function () {
          // ignore any "success" arguments when calling done
          done();
        }, done);
      }
    } else if (!doneCalled) {
      done();
    }

    return value;
  });
};

You could put this function in a helper.js or whatever you use to bootstrap your test suite. Thoughts? Are there any drawbacks to using this approach that you can think of?

@jmreidy

@domenic's Mocha as Promised offers a nice API around promises, but if you don't need the extra API and you're using Q, there's now a simple solution. As of 0.8.9, Q offers nend, which calls a node-style callback with the appropriate arguments when tacked onto a promise (and also forwards the promise). So promise.then(function (result) { assertionTest }).nend(done) makes async mocha tests work with a minimum of fuss. (Not that this helps you if you're not using Q!)

@domenic

@mjijackson that's basically what Mocha as Promised does, except it works for describe, it.only, and all the non-BDD interfaces as well.

@mjackson

@domenic Ah, good point. I had amended my previous code to handle it.only, beforeEach, and afterEach as well as it, but it doesn't handle the non-BDD interfaces. I like the work you've done in Mocha as Promised. Was just trying to think of alternative approaches. I think I will probably use your work going forward. Thanks!

@sricc

+1

@pahen

+1

@activars

+1 I would love to have Mocha supporting promise out of the box!

@n1k0

+1

@Nopik

+1

@jasonkuhrt

I appreciate @visionmedia's stated goal of keeping mocha lean and as agnostic as possible. @visionmedia could mocha expose a .use() type api so that people extend mocha in a way that doesn't suck? i.e. @domenic with mocha-as-promised has to:

Black magic. No, seriously, this is a big hack.

@domenic @kriskowal high-level what do you think of something like:

var mocha = require('mocha');
mocha.use(require('mocha-promises'));
@domenic

@jasonkuhrt I think that'd be fine and @visionmedia has already expressed some willingness to do so. Unfortunately the parts Mocha as Promised has to hook in to are pretty crazy, essentially needing to intercept any test function and replace it with a new version. While maybe Mocha could expose that, it's not exactly a high-demand plugin API.

@jasonkuhrt

@domenic It would probably help if @visionmedia had some of his own use-cases for such an api... : )

I myself cannot yet provide concrete use-cases for such a feature other than the promises issue at hand here : \

@rstacruz

Mocha promise testing is pretty easy.

Use a helper like this:

/**
 * Promise Test
 */
var pt = function(fn) {
  return function(done) {
    fn.apply(this).then(function(data) { done(); }, done);
  }
};

Now just decorate your promise-based tests with that helper:

it 'should work', pt ->
  Posts.findAll()
  .then (posts) -> assert.equal posts.length, 40
@wmertens

+1 CommonJS Promises/A should be first-class citizens in Mocha

@rstacruz Your helper eats the correct error location. How about

pt = (fn) -> (done) -> fn().then((-> done()), done)

This is basically what mocha-as-promised does, but it does it on everything.

@rstacruz

Corrected my comment.. that's much better indeed. I also used .apply(this) for less surprises.

@superstructor

Mocha is awesome. @visionmedia you may not think promises are also awesome, but a lot of Mocha's users do and need to test their promise based code. Please provide better extensibility than the black magic required in mocha-as-promised, or add first class support for promises to Mocha.

@tj
Owner
tj commented

@superstructor mocha works just fine with promises, especially with generators around the corner this won't matter at all. If anything the fact that promises are annoying here, illustrates that they are annoying :p

@wmertens

promises aren't annoying at all... you can write it('should foo', function(done) { return makeFooPromise().then(function(){done()}, done) }) but it's a lot more fun to write it('should foo', function() { return makeFooPromise() }) or even (coffeescript) it 'should foo', -> makeFooPromise().

Compare with it('should foo', function(done) { try { makeFoo(function(){done()}) } catch (e) { done(e) } }) and I personallly prefer reading and writing the mocha-as-promised version.

@thanpolas
it('should foo', function(done) { 
  return makeFooPromise().then(done, done);
});

If your promise rejects without an error object you should partial the last done

@wmertens

@thanpolas not quite, if the promise fulfills with a value it will call done(value) which makes mocha complain. I'm not sure what you mean by partial?

@thanpolas

@wmertens in that case you need to partial the first done too:

var _ = require('lodash');

it('should foo', function(done) { 
  return makeFooPromise().then(_.partial(done, null), _.partial(done, 'it did not foo'));
});
@wmertens

@thanpolas oh I see. Yes that works, you could create ok and nok wrappers for done that do the right thing. But the point was that it's much nicer to use mocha-as-promised :grinning:

@00Davo

The fact that promises are annoying here isn't indicative of promises being annoying; it's just indicative of the impedance mismatch between nodeback (err, res) APIs and promise APIs. Because they're not the same, there's some annoyance involved whenever the two need to interact.

The most common cases of interaction are encapsulated in the promise libraries to cut down on such annoyance – Q.nfcall is a prime example of this – but Mocha tests are not a common promise/nodeback interaction scenario. Mocha's API only expects nodeback interaction, so promise interaction suffers from impedance mismatch. Adding support for the promise style, which honestly is not that complicated (even given its need for black magic, mocha-as-promised completely achieves support in under 200 lines, and support in Mocha core would be much shorter), would eliminate this impedance mismatch.

@Bartvds

Is this still on?

Generators or not, it's weird to ignore Promises as then are pretty much standardised and I see them everywhere.

The 2 year long demand in this thread seems to indicate they are a real thing, and it feels a bit counter-intuitive to have to rely on mocha-as-promised with the author-admitted hack, or splash boilerplate code all over the suite to make this work (as you'd expect dryness and safety in tests, especially in the runner itself).

It would also improve reliability in all the mocha wrappers (like mocha-phantomjs, grunt-mocha and grunt-mocha-test).

@tj
Owner
tj commented

it is a community project so it's kinda up to you guys, it wasn't something I wanted to maintain but we've got additional maintainers now. What I'd hate to see is Mocha trying to solve every problem, become bloated, unmaintainable, and render itself useless

@Bartvds

True, that is a risk.

For now I went with a temporary fix that in use looks better then expected:

From within the suite I monkey-patched the it() case with an extra method, it.promised(), that contains all the promise handling boilerplate code (via Q) and then wraps it to regular callback type it().

Feels a bit dirty but seems to work ok.

A clean version of this could be a nice pattern, especially if it can be installed via a mocha.use() kind of interface that takes care of adding it safely (eg: make a skip/only/x version).

@00Davo

it.promised() doesn't sound like an appealing interface! If we need to be explicit about our use of promises anyway, putting it in Mocha core doesn't provide much advantage over a simple decorator like this:

function promised(fn) {
  return function(done) {
    fn.apply(this).then(function() {done();}, done);
  }
}

It's much friendlier for the regular it() to handle promise return values properly, as mocha-as-promised makes it do, and, again, it really doesn't take much code to achieve it.

Note that you don't need Q to enable promised tests; you don't need any promise library, since all that's needed to resolve a promised test case is the promise's then method, so adding promise support to Mocha would not require adding a promise library as dependency.

It would be nice to have some kind of mocha.use() plugin system, but it'd also be a lot more complex than promise support alone. Unless we have other use cases in mind, just adding the most-desired form of support is probably a better strategy than designing full-blown general plugin support.

@Bartvds

Implementation details aside (plenty options there), I must say I'm warming to the separate method pattern:

I think it has some practical benefits; like being able to assert if your promise-producing code is not accidentally returning undefined's instead of the expected promises (how would you catch this early in the 'overloaded' method?).

As experiment I converted some of my code to this it.promised() decorator hack and it looks just as tidy as the mocha-as-promised based originals (little more explicit).

Another question: what would be needed of mocha would later support generator-based async? I have no idea about how it'd work, but maybe we should keep it in mind.

If it requires custom code that doesn't mix with the regular code, old browsers or this promise code we can opt to also put it in its own method, like it.awaits() .It'll be extremely visible which case is running which async pattern (as no doubt people will mix).

@00Davo

Many applications of promise-producing test cases involve situations where you will definitely get a promise and not an undefined. For instance, all uses of Chai as Promised:

return thing.should.become("awesome");
return list.should.eventually.contain("pie");

It's certainly true that at times you might get an undefined slipping under the radar, though, so you've a point there. Whether or not it.promised() suites are as clean as overloaded-it() ones is pretty much entirely opinion; I think it's unnecessary overhead, personally, and currently with mocha-as-promised I use promise-returning suites almost exclusively so requiring a more complicated specification for promised test cases would be frustrating.

Also, "it will do the thing" is grammatical and "it promised will do the thing" is not. It feels kind of anti-BDD to mess up the sentence structure like that, especially for a modifier that certain codebases will be using a lot. Something like it.promises_that_it() (or it.promises.that.it()) would preserve the sentence structure but also be even more annoyingly long.

Currently generator async schemes require a wrapper around the generator function, like Q.async(); if that wrapper continues to be used for general generator-async use, then generators will work just fine with a promise-supporting Mocha. If we (the JS community) happen to shift to passing around generators directly instead of promises, there may need to be changes.

@Bartvds

I personally don't use Chai as Promised so much as it will replace the detailed AssertionErrors that might get thrown in the various callbacks with it's own generic 'expected xyz to resolve' message.

I'd rather assert more specific stuff on the actual values and get the nice diffs and all. I only use Chai as Promised to assert exact rejection reasons (even then usually not, because it limits flexibly in making assertions).

I don't think we can proscribe specific usage patterns, assume exclusive promise-returning suites or have mocha make the assumption there will be a promise as expected: we need to be pragmatic and support any usage style.

If the name of the method in BDD is a blocking issue then it becomes a simple linguistic problem. I think I"ll borrow eventually:

it.eventually("returns a valid document", () => {
    return testAPI.request(args).then((res) => {
        //assert fields and/or chain
    });
});

And eventually even works well as the alternative for test in the TDD interface.

@00Davo

I personally don't use Chai as Promised so much as it will replace the detailed AssertionErrors that might get thrown in the various callbacks with it's own generic 'expected xyz to resolve' message.
I'd rather assert more specific stuff on the actual values and get the nice diffs and all. I only use Chai as Promised to assert exact rejection reasons (even then usually not, because it limits flexibly in making assertions).

You can assert specific stuff on the actual values while using Chai as Promised, using .eventually. or more simply .become(), though. I recognise that .should.resolve assertions produce a generic and not exactly useful result, but those assertions are not really the main draw of the plugin anyway!

I don't think we can proscribe specific usage patterns, assume exclusive promise-returning suites or have mocha make the assumption there will be a promise as expected: we need to be pragmatic and support any usage style.

I'm a little confused by this part. How is Mocha making the assumption that there will be a promise, under the mocha-as-promised style? Isn't it making more of an assumption when you use something that makes it specifically expect a promise, like it.eventually()? And if we're going to support any usage style, why not support them implicitly if we can? We need an explicit (done) parameter for callback-based async, since otherwise the test case can't actually call done(); we don't need an explicit handling of promises, since Mocha can quite easily detect that it got a promise as return value and handle it appropriately.

If we really must use an explicit notation for promise cases, it.eventually() scans okay for me. Certainly a much better sentence structure than it.promised() produces.

The other problem I have with it.eventually() over the implicit mocha-as-promised method, though, is that a test case using promises is an implementation detail and not an external property of the test case. it.only() is a modifier that applies to Mocha's external handling of the test case, as is it.skip(). it.eventually() instead signifies that the case internally operates differently; as such, I think that makes it worse than using a wrapper it("does thing", promised(testCaseFunc)), since there is nothing externally fundamentally different between a case:

it("does a thing", function(done) {
  doTheThing().then(function() {done();}, done);
});

and a case:

it("does a thing", function() {
  return doTheThing();
});
@Bartvds

Ultimately Chai as Promised is just a convenient assertion wrapper, like the many other Chai plugins, they all only serves to tighten up the test code and give more useful error messages (you could essentially do everything they do with fancy assertions by using elementary assert(valid, message); and a boatload of glue.

I think we all agree it is nice to be flexible in how you assert (taste and cases vary). For example I know many people who use BDD suites with TDD assertions where you can't chain.

Mocha can quite easily detect that it got a promise as return value and handle it appropriately.

This is the thing. It can't really.

If we 'overload' the regular it() like in the mocha-as-promised style you are generalising and depend on mocha to find out what you exactly mean:

  • is this a classic sync-case that returns nothing (eg: undefined)? (elemental unit tests are still a large part of any suite)
  • is this supposed to be a promise case but we missed a return in the subject? (this is a real breakage that you want to catch in your testing)
  • is this supposed to be a promise case but we returned a non-promise? (this is also a real breakage)
  • is this value that got returned a random value, like from an early return or just silly user error (safety here can be enforced if we specify and assert regular cases to never return anything).
  • is this value a promise for special casing as per mocha-as-promised style.

If we implement promise support to its own method we can cover all of these and be very specific.

I don't see you point about difference in it.skip and it.eventually being different in modifying external or internal functionality. To me it looks like both are modifying mocha behaviour, the distinction is moot for end-users and it seems odd to put a conceptual limit there; especially with limited API real-estate (as besides describe, and it (and their TDD equivalents) mocha has no presence in the suites for us to use.

Then the wrapper you mention is really ugly, sure it works, but it creates noise in the code and is not integrated.

And of course there is a difference in your last code example: the top one has boilerplate code and will break if you forget or botch the done argument (as it'll be undefined). Not very dry or safe. If it has to be low tech then why stop there and not go for broke and use assert(boolean, message); everywhere.

So to sum up my case for a added method, like it.eventually:

  1. Explicit semantic (no need to visually parse the case, you see instantly it is about promises).
  2. Syntax matches existing behaviour modification features (skip etc).
  3. Not another set of distant wrapping parenthesis/braces (compared to a wrapper)
  4. It'll come from within mocha instead of being externally defined:
    1. allows more custom logic
    2. no carting around wrappers
    3. option to hook in promise specific fancy stuff (like listing pending promises on a timeout).
  5. Hardening of return value handing (as described above)
  6. No dependency in specifying done (and enforcing it is not used by mistake in the promise version)
  7. Have it built in so it'll work anywhere with zero integration problems.
  8. Keep maintainers happy by isolating the promise handling code somewhat.
  9. Nice extension pattern for upcoming async styles.
@activars

I actually agree with @visionmedia that mocha supposes to be a clean library: Mocha provides test functionality.
Mocha is not an assertion library.

Promise offers an alternative way to write callbacks. You probably should not forget that 'promise' is not a programming language feature, it's only a specification where everyone agrees on (not everyone implement the same spec).

I don't think this is mocha's responsibility to produce promise assertion, it's definitely some assertion libraries to solve these issues.

But what's lacking here is that mocha does not have public, or documented or standard interface/API/hook to allow third party assertion libraries to call the done function transparently.

If the interface/API/hook exists in mocha, then anyone could write a syntax that they prefer to use in the team or organization. If some other callback style is the next big thing, assertion library should reflect this feature.

@domenic

Promises are a language feature as of ES6. They are shipping in Chrome and Firefox nightlies already, and will be in the version of V8 included with Node 0.11.8+ (including Node 0.12.0).

@activars

@domenic Good to know! From design perspective, it's still an assertion library's job. The real problem is mocha does not publicly expose the hook?

@domenic

I don't feel this has anything to do with assertion libraries. Some tests are async. If those tests are written in ES5 style, they might take a done function or use waitsFor or something. If they are written in ES6 style they will return a promise. That feels like the domain of the test runner.

@Bartvds

@activars Not to be pedantic but the callback pattern is not really a true language feature either, it is just a convention that uses a more generic language feature (eg: function references and closures). See also that horrendous async construction jasmine v1 used to have (they switched to mocha-style done callback in v2).

And I think promises are a flow control feature just like callbacks (they are invented to be a replacement after all).

Note: this issue it is not about asserting promises themselves. Sure it does some of that but that is just to harden against mistakes.

It is about supporting to use promises to do flow control in the cases and also conveniently testing promise based async code (same as you can pass the done method as a callback to node.js style async code and have it react to the returned error).

@00Davo

@Bartvds

Then the wrapper you mention is really ugly, sure it works, but it creates noise in the code and is not integrated.

It absolutely does. I don't like it at all; I just think it's a better conceptual separation of concerns than the it.eventually() design.

And of course there is a difference in your last code example: the top one has boilerplate code and will break if you forget or botch the done argument (as it'll be undefined).

Obviously there is a difference, and obviously the top version is really bad because of boilerplate. However, there isn't really an external difference, in that they both express the same test case; the difference is only in terms of case-internal implementation details. Thus arises the internal-versus-external problem I have with it.eventually(). To put it another way:

  • it.skip() and it.only() change Mocha's handling of the test case.
  • it.eventually() changes Mocha's handling of the function that tests the case. Whether that function happens to use a promise or a callback seems, to me, an internal implementation detail.

… I am of course splitting hairs here. Let's be honest about it: I just really like using the mocha-as-promised style.

@Bartvds

@00Davo

Well, you can argue this 'test case' vs 'function that tests case' difference if you really must, but I think it is academic and I doubt regular users will care for this distinction as long as they get proper promise support soon (which is what this whole discussion is all about).

I really can't understand how it.eventually() could be considered so dramatically ugly.

It is almost identical as with mocha-as-promised you so admire, except it has just this simple injection of .eventually in the code (no hairy quotes, no long-range braces, no indents, no fancy syntax, just a dot and a identifier). Regular it() is not happening and there is not a lot of alternative or boilerplate syntax that is cleaner then this approach.

So let's stop arguing API UIX concerns, they are not the main problem in getting promise support: it is policy and implementation technicalities that blocked the it() pull-request for 2 years (and gave us mocha-as-promised as desperation solution), and these are what my proposal could offer to solve in a slick and expressive way.

I'll leave this as my attempt to get this rolling, awaiting alternate feedback.

@travisjeffery

i think it would be impractical to never support promises. we need a good implementation though. having every test check for a then method is nasty imo. out of the alternatives, i think a separate and explicit it.eventually like api is better — see the list at the bottom of this comment of pros that i pretty much all agree with.

@00Davo

Under the it.eventually() proposal, how are it.skip() and it.only() handled? it.eventually.only()? it.only.eventually()? Both?

@travisjeffery

you could chain only now, not skip but it'd just need a return added.

@tj
Owner
tj commented

I'm -1 for more method names personally, that would complicate things more than the return duck-typing, especially since mocha usually doesn't do anything with return values from the it callbacks.

@kumarharsh

After reading this, as well as TJ's reasoning for not supporting promises, I'd like to offer my :+1: to this PR.

Although mocha can be made to work with async tasks, as discussed here, it is certainly not elegant. And, it's not that JS is short of async functions that inclusion of promises would be an overkill. I hope that it's included in the future, so that ugly hacks are not required. :)

@demmer

+1 to the idea and to using duck-typing on .then instead of adding additional method names.

However one thing this doesn't handle is the case where the promise is rejected with something that's not an Error.

@domenic handles null or undefined in mocha-as-promised, but not the case where reject is called with something else like a string. IMO this should also be detected and turned into an Error.

@domenic

@domenic handles null or undefined in mocha-as-promised, but not the case where reject is called with something else like a string. IMO this should also be detected and turned into an Error.

Hmm interesting point. The only reason I handle null and undefined specially is because otherwise Mocha assumes it's a success. Otherwise I pass through any non-Error values to Mocha, which yells at the user for me.

@wmertens
@peleteiro

+1 CommonJS Promises/A should be first-class citizens in Mocha

@tj
Owner
tj commented

FWIW another reason promises shouldn't be added is that you'll soon be able to just use generators, so if we did eventually support promises via generators then there you would get 2 for 1. Once generators are supported there would be no reason to support promises in Mocha

@benjamingr

+1 for adding promise support.

Promises are now in ES6 - no reason to not add them in.

@Offirmo

+1 promises are now in the standard. Like it or not, they are here now.

My promise-based code is a pain to test with mocha. Throwing in a rejection callback is just not working, as exceptions are caught in promise internal code. Failed tests end up timing out...

@thanpolas

in the meantime...

var _ = require('lodash');

it('should foo', function(done) { 
  makeFooPromise().then(_.partial(done, null), _.partial(done, 'it did not foo'));
});
@thanpolas

Improving on the above example, when you have assertions, in order to see the thrown error properly vs the test timing out, you have to add one more .then(null, done); statement at the tail of your original promise:

test('Create a record', function(done) {
  model.create(fixture.one).then(function(data) {
    assert.equal(data.name, fixture.one.name, 'Name should be the same');
    done();
  }, done).then(null, done); // right here
});
@00Davo

@thanpolas But why mix promise async and callback async like that? You can just let the success or failure bubble up, like this:

test('Create a record', function(done) {
  model.create(fixture.one).then(function(data) {
    assert.equal(data.name, fixture.one.name, 'Name should be the same');
  }).then(done, done);
});
@thanpolas

@00Davo that's even better :+1:

@giggio

+1
Or at least make Mocha more extensible to allow it to be plugged in without a hack.
This is the very reason I abandoned Jasmine: they got stuck in the past. I hope it doesn't happen to Mocha.

@Bartvds

@visionmedia @travisjeffery Could you advise your users how many more +1's we need to get real promise support in mocha, if at all?

The spec for ES6 promises is out there, let's not ignore it.

Holding out until generators is so awkward as that would not work for old browsers. There are plenty of suggestions and options here in the thread to get this working for all environment that can currently run mocha.

If it is just a matter of your attention having moved on to other projects then it would be great to assign a maintainer with publishing rights to decide on these things, as there are still a lot of mocha users who'd like to move on with the times (but not be limited by generator support, as our clients' IE9 user-base is still paying many bills).

If this cannot be realised in this project even with so many request and no real technical roadblock, then would it be a better idea to leverage the MIT licence, fork and start a mocha2? I'd hate to schism the community but if this line is going stale then it might be our best option.

@tj
Owner
tj commented

@Bartvds I already said "it is a community project so it's kinda up to you guys"

we could maybe use another maintainer or two, @travisjeffery has been pretty on top of things though! but if anyone else is interested I'd be open to that

@Bartvds

Cool, thanks for confirming that. From the +1's I get the feeling that the community would like to see this happen. But it is still hanging in limbo by lack of concrete decision (Yes/No/Wait).

@travisjeffery When I read back I see you are positive about promise support: if you still like to see this happening and think we have enough community support then please hammer the consensus so we can move out of limbo and on to the implementation details.

@giggio

If it is up to the community and it seems there is a lot of support, why not add the option to have promises at once?
What exactly needs to be done? Just saying it is up to the community does not cut it. What exactly are you waiting for so this is done, @visionmedia?

@xaka

+1. As it was pointed out earlier here, ES6 does support the promises already, so i see no reason to keep fighting against the reality. Just let it go. Please. The PR itself looks very simple and makes no harm. @travisjeffery?

@tj tj commented on the diff
lib/runnable.js
((5 lines not shown))
try {
- if (!this.pending) this.fn.call(ctx);
- this.duration = new Date - start;
- fn();
+ if (!this.pending) {
@tj Owner
tj added a note

this part would be a lot nicer if we did something like toCallback(result) below, lots of nested conditionals are really confusing to read

@domenic
domenic added a note

Hmm will give it a shot at factoring it out.

@travisjeffery Owner

something like?

  function toCallback(result) {
    if (result && typeof result.then === 'function') {
      result.then(
        function(){
          done();
        },
        done
      );
    } else {
      done();
    }
  }

  // sync
  try {
    if (!this.pending) {
      toCallback(this.fn.call(ctx));
    } else {
      done();
    }
  } catch (err) {
    done(err);
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
@xaka

Thank you guys! I'd love to use it right away and remove all those hack-ish packages. When do you think the new mocha version will be released with promises support?

@domenic domenic referenced this pull request from a commit in domenic/mocha
@domenic domenic Fix up promise handling.
Per comments in #329 we can factor out the calling into its own function.

I also reused `this.resetTimeout()` instead of manually setting it, and added the timeout for promise case as well.
9ee9bb0
@domenic domenic referenced this pull request from a commit in domenic/mocha
@domenic domenic Fix up promise handling.
Per comments in #329 we can factor out the calling into its own function.

I also reused `this.resetTimeout()` instead of manually setting it, and added the timeout for promise case as well.
344f29a
@travisjeffery travisjeffery referenced this pull request from a commit
@domenic domenic Fix up promise handling.
Per comments in #329 we can factor out the calling into its own function.

I also reused `this.resetTimeout()` instead of manually setting it, and added the timeout for promise case as well.
17cfdee
@travisjeffery

1.18.0 is out now with promise support.

@benjamingr

Awesome! Great work - very good news.

@Bartvds

Wow, this is excellent! Thanks for making it happen.

@azu azu referenced this pull request in azu/mocha-support-promise
Closed

mocha support promises. #1

@bajtos

Awesome! :v:

@cscott cscott referenced this pull request from a commit in cscott/prfun
@cscott cscott Tweak tests to use Promise support in mocha 1.18.
Mocha now supports tests returning promises directly, so let's do that!
mochajs/mocha#329 (comment)
dc3a7fd
@xogeny

Very cool. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Mar 16, 2012
  1. @domenic

    Adding promise support to runnables (and thus tests).

    domenic authored
    * If a runnable returns a duck-typed promise, i.e. an object with a `then` method, it gets treated as an async test, with success if the promise is fulfilled and failure if it is rejected (with the rejection reason as the error).
    * Includes tests both of new functionality and some to show that introducing this doesn't break any old functionality.
This page is out of date. Refresh to see the latest.
Showing with 91 additions and 9 deletions.
  1. +24 −9 lib/runnable.js
  2. +67 −0 test/runnable.js
View
33 lib/runnable.js
@@ -107,14 +107,15 @@ Runnable.prototype.run = function(fn){
, finished
, emitted;
- // timeout
+ // timeout: set the timer even if this.async isn't true, since we don't know
+ // ahead of time for the promisey case
if (this.async) {
- if (ms) {
- this.timer = setTimeout(function(){
+ this.timer = setTimeout(function(){
+ if (!finished) {
done(new Error('timeout of ' + ms + 'ms exceeded'));
self.timedOut = true;
- }, ms);
- }
+ }
+ }, ms);
}
// called multiple times
@@ -152,11 +153,25 @@ Runnable.prototype.run = function(fn){
}
// sync
+ var result;
try {
- if (!this.pending) this.fn.call(ctx);
- this.duration = new Date - start;
- fn();
+ if (!this.pending) {
@tj Owner
tj added a note

this part would be a lot nicer if we did something like toCallback(result) below, lots of nested conditionals are really confusing to read

@domenic
domenic added a note

Hmm will give it a shot at factoring it out.

@travisjeffery Owner

something like?

  function toCallback(result) {
    if (result && typeof result.then === 'function') {
      result.then(
        function(){
          done();
        },
        done
      );
    } else {
      done();
    }
  }

  // sync
  try {
    if (!this.pending) {
      toCallback(this.fn.call(ctx));
    } else {
      done();
    }
  } catch (err) {
    done(err);
  }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ var result = this.fn.call(ctx);
+
+ if (result && typeof result.then === "function") {
+ result.then(
+ function(){
+ done(); // don't pass through any non-error fulfillment values
+ },
+ done // pass through any errors
+ );
+ } else {
+ done();
+ }
+ } else {
+ done();
+ }
} catch (err) {
- fn(err);
+ done(err);
}
};
View
67 test/runnable.js
@@ -176,5 +176,72 @@ describe('Runnable(title, fn)', function(){
})
})
+ describe('when fn returns a promise', function(){
+ describe('when the promise is fulfilled with no value', function(){
+ var fulfilledPromise = {
+ then: function (fulfilled, rejected) {
+ process.nextTick(fulfilled);
+ }
+ };
+
+ it('should invoke the callback', function(done){
+ var test = new Runnable('foo', function(){
+ return fulfilledPromise;
+ });
+
+ test.run(done);
+ })
+ })
+
+ describe('when the promise is fulfilled with a value', function(){
+ var fulfilledPromise = {
+ then: function (fulfilled, rejected) {
+ process.nextTick(function () {
+ fulfilled({});
+ });
+ }
+ };
+
+ it('should invoke the callback', function(done){
+ var test = new Runnable('foo', function(){
+ return fulfilledPromise;
+ });
+
+ test.run(done);
+ })
+ })
+
+ describe('when the promise is rejected', function(){
+ var expectedErr = new Error('fail');
+ var rejectedPromise = {
+ then: function (fulfilled, rejected) {
+ process.nextTick(function () {
+ rejected(expectedErr);
+ });
+ }
+ };
+
+ it('should invoke the callback', function(done){
+ var test = new Runnable('foo', function(){
+ return rejectedPromise;
+ });
+
+ test.run(function(err){
+ err.should.equal(expectedErr);
+ done();
+ });
+ })
+ })
+ })
+
+ describe('when fn returns a non-promise', function(){
+ it('should invoke the callback', function(done){
+ var test = new Runnable('foo', function(){
+ return { then: "i ran my tests" };
+ });
+
+ test.run(done);
+ })
+ })
})
})
Something went wrong with that request. Please try again.