#resource/technology Use `fakeAsync` & `waitForAsync` ## Potential Problems - expects not running - timeouts ```ts describe("Async", () => { it("should use window.setTimeout", ()=> { let a = 1; window.setTimeout(() => (a = a + 1)); expect(a).toBe(2); }) }); ``` -> problems with async, test already exited when the code is executed (test is passed, but the expect is not executed) - every time when you are writing tests make sure that the test fails (with expected reason) - to ensure that the async works, and than fix it - to ensure that the assertions are called: beforeEach(() => expect.hasAssertions()) - don't rely on it 100%, there could be also exceptions - use fake timers ([[Angular]] testing tools (not Jest fake timers)) ```ts it("should use Promise", () => { let a = 1; Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); })); }); ``` - failed, but not with expected reason, - something with micro vs macro task - there is difference between window.setTimeout and the Promise. - with Promise there is still problem with asynchronity ## How to deal with that? ### Native Approaches - `done` callback - return `Promise` - `expect().resolves` - `async`/`await` #### Done Callback ```ts it("should use the done callback", (done: DoneCallback) => { let a = 1; Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); done(); }); }); ``` - test will not stop until done function is called (but only when expect is passed) - but done callback is also not good because error message are not telling us what we expect - we run into timeout - expect fails and runs into timeout, done function is not called - you have to wait for 5 seconds -> tests are slow then ```ts it("should use the done callback", (done: DoneCallback) => { let a = 1; Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); }) .then(done, done); }); ``` - for Promise, not for the window.setTimeout - one older solution #### Return Promise ```ts it("should return Promise", () => { let a = 1; return Promise.resolve().then(() => { a = a + 1; expect(a).toBe(2); }); }); ``` #### expect(promise).resolves ```ts it("should return Promise", () => { let a = 1; const promise = Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); }); return expect(promise).resolves.toBe(2); }); ``` #### async / await - modern solution ```ts it("should use async/await", async () => { let a = 1; const promise = Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); }); expect(await promise).toBe(2); }); ``` - function returns promise that resolves nothing ### Angular-based Approaches two important functions provided by Angular: - waitForAsync - fakeAsync - flushMicrotasks: run all Microtasks - tick: move forward in time - flush: run all asynchronous tasks #### waitForAsync ```ts it("should use waitForAsync", waitForAsync(() => { let a = 1; Promise.resolve().then(() => { a = a + 1; expect(a).toBe(1); }); }); })); ``` - problem with `waitForAsync` or `fakeAsync` - Angular has access to all the threads that were started, and waits for everything (micro and macro tasks) - window.setX methods are macro tasks - test that use window.setTimeout test that interacts with this one - this is the nightmare! - we should not allow tests to influence each other -> fix window.setTimeout by adding waitForAsync - if still errors, fix it with: - Go to `package.json` - add another parameter `--detect-open-handlers` for `test:watch` script - however, tests become slower (that's why it is only in `--watch` method, not for everything) - however, disadvantage is that you can see the strange error message, with `--watch` you can see the real error message and fix it - `waitForAsync` cannot be applied always, that's why we have also `fakeAsync` #### fakeAsync ```ts it("should use fakeAsync", fakeAsync(() => { let a = 1; window.setTimeout(() => { a = a + 1; expect(a).toBe(2); }); }); })); ``` - behave little bit differently, - inside the test macro task is defined, but not executed - execute with `tick()` ```ts it("should use fakeAsync", fakeAsync(() => { let a = 1; window.setTimeout(() => { a = a + 1; }); tick(); expect(a).toBe(2); }); })); ``` - `flushMicrotasks()` - runs only micro tasks When to use `waitForAsync` vs `fakeAsync`? - execution times are very similar - `fakeAsync` is generally preferred solution to decide when to trigger async and you can always have the `expect` as the last command - `fakeAsync` will give us to have a possibility of AAA (arrange, act, assert) structure in the tests - run all tasks immediately -> `flush()` - however, in case that there are some restarts of threads when finished, it won't work - destroy needs to be called explicitly (some 3rd party libraries do the restarts) ### Observables - Problem with Observables - combined with [[Jest]], - not with Jasmine -> Jasmine uses done callback inside of subscription (waits for timeout -> longer tests) ~~~ts it("should use Observables", () => { let a = 1; of(1).subscribe(n => a = a + n); expect(a).toBe(1); }) ~~~ - pitfall: `of` uses observables synchronously - it depends on the way how the values are emitted - subscribe also runs synchronously - even with synchronous Observables we have a potential for asynchronicity -> use `fakeAsync` (or `waitForAsync`) - don't map observable to promise with `of(1).toPromise()` (deprecated RxJs 7) use `await firstValueFrom(of(1))`, `await lastValueFrom(of(1))` instead - because observable could not always have a return value (if returned immediately) (promise always needs one)