moll / js-must

An assertion library for JavaScript and Node.js with a friendly BDD syntax (awesome.must.be.true()). It ships with many expressive matchers and is test runner and framework agnostic. Follows RFC 2119 with its use of MUST. Good stuff and well tested.
Other
336 stars 35 forks source link

Fulfill or betray promises #68

Open jandockx opened 7 years ago

jandockx commented 7 years ago

I recently switched to must from chai, because I started to used Standardjs. I do find the reasons why you created must meaningful ("Beware of libraries that assert on property access", "RFC 2119", …), and find myself happier with must than chai.

In both cases, I am frustrated with testing Promises.

The current must.resolve and must.reject primitives lead me to complex coding structures, embedded in Promise.all([]) constructs far to often (the same applies to chai and its extensions).

This is an example of a Mocha test:

const Gatherer = …
const domain = …
const getDataFailure = function(…) { … }

it('fails as described with a getData that fails', function () {
  const subject = new Gatherer({domain: domain, getData: getDataFailure})
  subject.domain.must.equal(domain)
  subject.getData.must.equal(getDataFailure)
  const initialised = subject.initialise(at)
  return Promise.all([
    initialised.must.reject,
    initialised.catch(err => {
      err.must.be.an.error(Error, /getting data/)
      subject.domain.must.equal(domain) // did not change
      subject.getData.must.equal(getDataFailure) // did not change
    })
  ])
})

Note that the initialised.must.reject phrase doesn't work. There is no simple way to express that an AssertionError should be thrown when the promise resolves, without doing something with this (initialised.must.reject.to…). To work, we would need something of the form initialised.must.reject(), i.e., a method call.

If I want to say more about the returned value or error, and about the state of other things after the promise settles, I seem to be forced to use complex Promise.all([]) constructs, and variables to store intermediate results.

In this PR, I offer an alternative that seems to work in my tests. We express that a Promise must 'fulfill' or 'betrays' its intentions. The check fails if the Promise does not settle in the way it must. If it does, the conditions that we express in the 'fullfil' or 'betray' argument are tested.

The whole can be returned in a test, which is the most pleasing way most test frameworks support asynchronous tests.

With these new methods, the above test now becomes:

it('fails as described with a getData that fails', function () {
  const subject = new Gatherer({domain: domain, getData: getDataFailure})
  subject.domain.must.equal(domain)
  subject.getData.must.equal(getDataFailure)
  return subject.initialise(at).must.betray(err => {
    err.must.be.an.error(Error, /getting data/)
    subject.domain.must.equal(domain) // did not change
    subject.getData.must.equal(getDataFailure) // did not change
  })
})

There is much less accidental complexity in this form.

More examples are in the JS Doc.

I you find to time, it would be nice if you could review this solution, and give me any feedback that you deem relevant.

jandockx commented 7 years ago

Travis fails on this PR only for node 0.10 (#145.2).

The 2 tests that fail are each other's mirror, for fulfill and betray:

1) Must.prototype.betray AssertionError must have all properties when it fails because of a resolution:
    Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

2) Must.prototype.fulfill AssertionError must have all properties when it fails because of a rejection:
    Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

I don't understand why. In all branches, it seems that the done() callback is called, or an assert fails. This is confirmed by the fact that tests on all other versions of node work.

Does anybody have any ideas about what is happening?

moll commented 6 years ago

Wow, thank you for doing that much work! I must've entirely forgotten about this and was notified by GitHub mentioning you added v8 to .travis.yml. I hope to review you what you've done soon!