Open aslakhellesoy opened 7 years ago
with the assumption that synchronous tests are faster than asynchronous ones.
Synchronous tests are generally, but not essentially faster, agreed. However they are also in control in a way that asynchronous tests aren't. They fail faster. When a FinishedPromise
fails, it fails when the promise is created, with a stack trace that leads back to why... instead of failing later when an unrelated promise is resolved or rejected. This is an important distinction that makes test failures easier to reason about. Also async tests are just harder to write.
The only way to prevent I/O and sleeps is to throw errors when they happen
The thing is, with synchronous tests, you don't need to prevent I/O, it's simply irrelevant. If a test depends on I/O, it will necessarily fail, because the test is synchronous and won't wait for the I/O to occur. So this is actually much easier that any preventing.
But you're absolutely right this is ultimately about achieving faster (and simpler) tests.
I'm still not sure we actually need this thing to achieve that goal, either, but not because we can still be async if we want to. I'm wondering if the core of your typical application code is actually async...
Ideally there should be a clean separation between the pure and impure (async or I/O) parts of your code anyway. I think this implies that the core of your application doesn't necessarily need to know about Promises or async/await keywords. Just as functional programmers like to make a very explicit separation between these two worlds (e.g. with monads in haskell, which I won't pretend to understand very well!) I wonder if we can structure JavaScript with asynchronous stuff on the outside (streaming responses to clients, requesting records from databases) but pure stuff on the inside. Can't the essence of the application look (and be tested as though it is) synchronous, even if it is ultimately deployed in the middle of an asynchronous world? This is something I am exploring now...
One other thing that might be interesting when considering whether it's useful to override Promise behaviour in tests...
So, a FinishedPromise
resolves or rejects when created, because it's finished (not happy with the name BTW).
I am also experimenting with "dangling" promises that are left unresolved and then explicitly resolved or rejected by the test after instantiation. If a test depends on multiple asynchronous operations, it can be difficult to test permutations of those operations resolving/rejecting in different orders. Making a test explicitly resolve or reject the promises it depends on is one potential solution to this problem and might be a better reason to use fakes...
You can see an example of synchronously testing Promise.race
here: https://github.com/featurist/banana-shark/blob/master/examples/promise-race.js
My understanding is that this library is intended to be used in conjuction with synchronous tests, with the assumption that synchronous tests are faster than asynchronous ones.
I'm not sure synchronicity is required to achieve faster tests. For slow tests, the time spent doing I/O is likely to be orders of magnitude longer than the time spent waiting for non-I/O function calls to execute higher up the execution stack.
If I/O and sleeps (
setTimeout
) can be eliminated completely I think tests will be very fast, even if they are asynchronous. This is easy to verify - just run some tests that generate a large number of (1000s) chained promises performing trivial (but realistic) operations that don't sleep and don't do any I/O.The only way to prevent I/O and sleeps is to throw errors when they happen. This can be done by overriding the behaviour of low-level I/O APIs such as
fs
andnet
. In contrast withPromise
, this is easy to do because there is no syntax translation likeasync
/await
that prevents this from being done.