Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

A prototypical animal which looks like an A+ Promise but doesn't defer immediately, so can run synchronously, for testing

License

NotificationsYou must be signed in to change notification settings

fluffynuts/synchronous-promise

Repository files navigation

TL;DR: A prototypical animal which looks like an A+ Promise but doesn't deferimmediately, so can run synchronously, for testing. Technically, this makes itnot A+ compliant, since part of the A+ spec is that resolution be asynchronous.

This means that I unfortunately can't run the official tests athttps://github.com/promises-aplus/promises-tests. As such, I rely on issue reports from users and welcome contributions.

Build and Test

npm

Ifsynchronous-promise has made something easier for you and you'd like to say thanks,check out my sponsors page.

Why?

The standard ES6 Promise (and any others whichare A+ compliant) push the promise logic to the backgroundimmediately, departing from the mechanisms employed in years past by promiseimplementations in libraries such as jQuery and Q.

This is a good thing -- for production code. But it can make testing moreconvoluted than it really needs to be.

Often, in a test, we're stubbing out a function which would return a promise(eg http call, show a modal dialog requiring user interaction) with a promisethat resolves immediately, eg (using, mocha/sinon/chai):

describe('the thing',()=>{it('will do some stuff',()=>{// ArrangeconstasyncLibraryFake={someMethod:sinon.stub().returns(Promise.resolve('happy value!'))},sut=createSystemUnderTestWith(asyncLibraryFake);// Actsut.doSomethingInteresting();// Assert//  [*]})});

[*] Ideally, we'd just have assertions here, but the code above has backgrounded,so we're not going to get our expected results unless we employ async testingstrategies ourselves.

One strategy would be to return the promise fromasyncLibraryFake.someMethodfrom thedoSomethingInterestingfunction and perform our asserts in there:

describe('the thing',()=>{it('will do some stuff',done=>{// ArrangeconstasyncLibraryFake={someMethod:sinon.stub().returns(Promise.resolve('happy value!'))},sut=createSystemUnderTestWith(asyncLibraryFake);// Actsut.doSomethingInteresting().then(()=>{// Assertdone()});})});

And there's nothing wrong with this strategy.

I need to put that out there before anyone takes offense or thinks that I'm suggestingthat they're "doing it wrong".If you're doing this (or something very similar), great;async/await, if available,can make this code quite clean and linear too.

However, when we're working on more complex interactions, eg when we're nottesting the final result of a promise chain, but rather testing a side-effectat some step during that promise chain, this can become more effort to test(and, imo, make your testing more unclear).

Many moons ago, using, for example, Q, we could create a deferred object withQ.defer() and then resolve or reject ith withdeferred.resolve() anddeferred.reject(). Since there was no initial backgrounding, we could setup a test with an unresolved promise, make some pre-assertions, then resolveand make assertions about "after resolution" state, without making our testsasync at all. It made testing a little easier (imo) and the idea has beenpropagated into frameworks likeangular-mocks

Usage

SynchronousPromise looks (from the outside) a lot like an ES6 promise. We constructthe same:

varpromise=newSynchronousPromise((resolve,reject)=>{if(Math.random()<0.1){reject('unlucky!');}else{resolve('lucky!');}});

They can, of course, be chained:

varinitial=newSynchronousPromise((resolve,reject)=>{resolve('happy!');});initial.then(message=>{console.log(message);})

And have error handling, either from the basic A+ spec:

initial.then(message=>{console.log(message);},error=>{console.error(error);});

Or using the more familiarcatch():

initial.then(message=>{console.log(message);}).catch(error=>{console.error(error);})

.catch() starts a new promise chain, so you can pick up with new logicif you want to..then() can deal with returning raw values or promises(as per A+)

SynchronousPromise also supports.finally() as of version 2.0.8.

Statics

.all(),.resolve() and.reject() are available on theSynchronousPromiseobject itself:

SynchronousPromise.all([p1,p2]).then(results=>{// results is an array of results from all promises}).catch(err=>{// err is any single error thrown by a promise in the array});SynchronousPromise.resolve('foo');// creates an already-resolved promiseSynchronousPromise.reject('bar');// creats an already-rejected promise

(race() isn't because I haven't determined a good strategy for that yet,considering the synchronous design goal -- but it'sunlikely you'll needrace() from a test).

Extras

SynchronousPromise also provides two extra functions to make testing a littleeasier:

Static methods

Theunresolved() method returns a new, unresolvedSynchronousPromise withthe constructor-function-providedresolve andreject functions attached as properties.Use this when you have no intention of resolving or rejecting the promise or when youwant to resolve or reject at some later date.

varresolvedValue,rejectedValue,promise=SynchronousPromise.unresolved().then(function(data){resolvedValue=data;}).catch(function(data){rejectedValue=data;});// at this point, resolved and rejected are both undefined// ... some time later ...if(Math.random()>0.5){promise.resolve("yay");// now resolvedValue is "yay" and rejectedValue is still undefined}else{promise.reject("boo");// now rejectedValue is "boo" and resolvedValue is still undefined}

Instance methods

pause() pauses the promise chain at the point at which it is called:

SynchronousPromise.resolve('abc').then(data=>{// this will be runreturn'123';}).pause().then(data2=>{// we don't get here without resuming});

andresume() resumes operations:

varpromise=SynchronousPromise.resolve('123').pause(),captured=null;promise.then(data=>{captured=data;});expect(captured).to.be.null;// because we paused...promise.resume();expect(captured).to.equal('123');// because we resumed...

You can usepause() andresume() to test the state of your system undertest at defined points in a series of promise chains

ES5

SynchronousPromise is purposefully written with prototypical, ES5 syntax so youcan use it from ES5 if you like. Use thesynchronous-promise.js file from thedist folder if you'd like to include it in a browser environment (eg karma).

Typescript

Thesynchronous-promise package includes anindex.d.ts. To install, run:

typings install npm:synchronous-promise --save

On any modern TypeScript (v2+), you shouldn't need to do this.

Also note that TypeScript does async/await cleverly, treating all promisesequally, such thatawait will work just fine against a SynchronousPromise -- it just won't be backgrounded.

HOWEVER: there is avery specific way that SynchronousPromisecan interfere with TypeScript: if

  • SynchronousPromise is installed globally (ie, overriding thenativePromise implementation) and
  • You create a SynchronousPromise which is resolved asynchronously,eg:
global.Promise=SynchronousPromise;awaitnewSynchronousPromise((resolve,reject)=>{setTimeout(()=>resolve(),0);});// this will hang

This is due to how TypeScript generates the__awaiter functionwhich isyielded to provideasync/await functionality, inparticular that the emitted code assumes that the globalPromisewillalways be asynchronous, which is normally a reasonable assumption.

Installing SynchronousPromise globally may be a useful testing tactic,which I've used in the past, but I've seen this exact issue crop upin production code.SynchronousPromise therefor also provides two methods:

  • installGlobally
  • uninstallGlobally

which can be used if your testing would be suited to havingPromise globallyoverridden bySynchronousPromise. This needs to get the locally-available__awaiter and the result (enclosed with a reference to the realPromise)must override that__awaiter, eg:

declarevar__awaiter:Function;beforeEach(()=>{__awaiter=SynchronousPromise.installGlobally(__awaiter);});afterEach(()=>{SynchronousPromise.uninstallGlobally();});

It's not elegant that client code needs to know about the transpiledcode, but this works.

I have an issue open on GitHubmicrosoft/TypeScript#19909but discussion so far has not been particularly convincing thatTypeScript emission will be altered to (imo) a more robustimplementation which wraps the emitted__awaiter in a closure.

Production code

The main aim of SynchronousPromise is to facilitate easier testing. That beingsaid, it appears to conform to expectedPromise behaviour, barring thealways-backgrounded behaviour. One might be tempted to just use it everywhere.

However: I'd highly recommend usingany of the more venerable promise implementationsinstead of SynchronousPromise in your production code -- preferably the vanillaES6 Promise, where possible (or the shim, where you're in ES5). Or Q.Or jQuery.Deferred(), Bluebird or any of the implementations athttps://promisesaplus.com/implementations.

Basically, this seems to work quite well for testing andI've tried to implement every behaviour I'd expect from a promise -- but I'mpretty sure that a nativePromise will be better for production code any day.

About

A prototypical animal which looks like an A+ Promise but doesn't defer immediately, so can run synchronously, for testing

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors9


[8]ページ先頭

©2009-2025 Movatter.jp