The text you are about to read describes an imaginary dialog between two programmers on their way to discover the of a library called . orgastic pleasures asyncFn asyncFn provides additional methods to eg. or to introduce “ ” for the promises returned by mock functions. This simplifies async unit testing by allowing tests that read chronologically, . jest.fn sinon.spy late resolve like a story From this, we get a of structuring and writing unit tests. new perspective Foreword Be warned that we’ll also cover topics such as , , , and “ ”. They are not the main topic here, but some words are spared for them nonetheless to justify . TDD pair programming evil pairing negation testing a way of thinking Best of luck, . Dear Reader Chapter 1: The premise and the foundation : My name is Fry, and I find it difficult to unit test async-stuff in JavaScript. Fry : Tell me more. Leela : Yup, say I wanted to implement something to this specification: Fry : As a player, I can play a game called Monster Beatdown : I can damage an encountered monster I encounter a monster I choose to attack it the monster loses hit points : I can try flee a monster I encounter a monster I choose to flee the monster eats me I lose : I can win the game by beating a monster until it is knocked out I encounter a monster I attack it until it has no hit points the monster is knocked out I win Feature Scenario Given And Then Scenario Given And Then And Scenario Given When Then And : The kicker here is that in this game, prompting the player for action is . Fry asynchronous : Mm-hmm, I think I get it. Let’s get our hands dirty and see where it lands us. Leela : Here’s an initial test for good measure: Fry gameWithoutDependencies ; it( , () => { messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : messagePlayerMock, }); game.encounterMonster(); expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); import from './monsterBeatdown' 'when a monster is encountered, informs the player of this' const const messagePlayer 'You encounter a monster with 5 hit-points.' Contents of so far: ./monsterBeatdown.js ({ messagePlayer }) => ({ : { messagePlayer( ); }, }); export default encounterMonster => () `You encounter a monster with 5 hit-points.` : Ok, so far so good. I see you chose to the mock-function for messaging the player as an argument for the function we are testing. Crystal. Full steam ahead. Leela inject : Right on. Next up will be my vision for asking the player if they want to attack, and if not, they lose. Fry Chapter 2: The problems emerge gameWithoutDependencies ; it( , () => { messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : messagePlayerMock, }); game.encounterMonster(); expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { askPlayerToHitMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, }); game.encounterMonster(); expect(askPlayerToHitMock).toHaveBeenCalledWith( ); }); it( , () => { askPlayerToHitMock = jest.fn( .resolve( )); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); game.encounterMonster(); expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { askPlayerToHitMock = jest.fn( .resolve( )); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); game.encounterMonster(); expect(messagePlayerMock).toHaveBeenCalledWith( ); }); import from './monsterBeatdown' 'when a monster is encountered, informs the player of this' async const const messagePlayer await 'You encounter a monster with 5 hit-points.' 'when a monster is encountered, asks player to attack' async const const askPlayerToHit await 'Do you want to attack it?' 'given a monster is encountered, when player chooses to flee, the monster eats the player' async // When asked to attack, choosing false means to flee. const => () Promise false const const askPlayerToHit messagePlayer await 'You chose to flee the monster, but the monster eats you in disappointment.' 'given a monster is encountered, when player chooses to flee, the game is over' async const => () Promise false const const askPlayerToHit messagePlayer await 'Game over.' Contents of so far: ./monsterBeatdown.js ({ messagePlayer = {}, askPlayerToHit = {} }) => ({ : { messagePlayer( ); messagePlayer( ); askPlayerToHit( ); messagePlayer( , ); }, }); export default => () => () encounterMonster => () // For real life, the implementation below makes little sense. // This is intentional, and is so to prove a point later. // At this point, it suffices that all tests are green 'Game over.' 'You encounter a monster with 5 hit-points.' 'Do you want to attack it?' 'You chose to flee the monster, but the monster eats you in disappointment.' : Hold the phone. I see two problems here. One is the , and the other one is about the test describing occurrences in , making the test harder to read. Let’s start by fixing the first problem Leela duplication non-chronological order . Chapter 3: The clumsy fix : Uh, I was just about to remove the duplication anyway. , right? Fry Red-green-refactor : Right. Leela gameWithoutDependencies ; describe( , () => { askPlayerToHitMock; messagePlayerMock; game; beforeEach( { askPlayerToHitMock = jest.fn(); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); }); it( , () => { game.encounterMonster(); expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { game.encounterMonster(); expect(askPlayerToHitMock).toHaveBeenCalledWith( , ); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.mockResolvedValue( ); game.encounterMonster(); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); }); }); import from './monsterBeatdown' 'given a monster is encountered' let let let => () askPlayerToHit messagePlayer 'informs the player of this' async await 'You encounter a monster with 5 hit-points.' 'asks player to attack' async await 'Do you want to attack it?' 'when player chooses to flee' async // When asked to attack, choosing false means to flee. false await 'player is informed of the grim outcome' 'You chose to flee the monster, but the monster eats you in disappointment.' 'the game is over' 'Game over.' Contents of so far: ./monsterBeatdown.js // no changes, as only tests were refactored. : There. Now the duplication for the test setup has been removed, but only to a degree. However, the other problem of test not being written in chronological order prevents us from removing all the duplication. See how we are forced to repeat because needs to know how to behave before it is called. Worse yet, this makes the “describe” dishonest, as it claims to display how " ", but in reality, this is something that happens only later in the test setup, if ever. All this kind of bums me out, and I’ve felt the pain of this getting out of hand as in real-life requirements and features start to pile up. I know many ways to ease the pain a little bit, but nothing I’ve tried has left me comfortable. Fry game.encounterMonster() askPlayerToHitMock given a monster is encountered : I see. Let’s now introduce as an expansion to the normal to tackle all. Behold. Leela asyncFn jest.fn Chapter 4: The beholding of asyncFn gameWithoutDependencies ; asyncFn ; describe( , () => { askPlayerToHitMock; messagePlayerMock; game; beforeEach( { askPlayerToHitMock = asyncFn(); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); game.encounterMonster(); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(askPlayerToHitMock).toHaveBeenCalledWith( , ); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.resolve( ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); }); }); import from './monsterBeatdown' import from '@asyncFn/jest' 'given a monster is encountered' let let let => () // Note: Before we used jest.fn here instead of asyncFn. askPlayerToHit messagePlayer 'informs the player of this' async 'You encounter a monster with 5 hit-points.' 'asks player to attack' // Note how even if askPlayerToHitMock is now a mock made using asyncFn(), it still is a jest.fn(). This means eg. toHaveBeenCalledWith can be used with it. 'Do you want to attack it?' 'when player chooses to flee' async // Note how choosing false here still means fleeing combat, but here we use asyncFn's .resolve() to control the outcome. And then we await for the consequences of that. await false 'player is informed of the grim outcome' 'You chose to flee the monster, but the monster eats you in disappointment.' 'the game is over' 'Game over.' Contents of so far: ./monsterBeatdown.js // no changes, as only tests were refactored. : Now the duplication is gone, and everything takes place in clean, chronological order. It kind of reads like a story, don’t you think? Leela : Mm-hmm. I see it. I have a good feeling about it. But something is bothering me with the production code. I see that the tests are all green, but clearly, the code does not do anything sensical. It just blows through, merely satisfying the tests. Fry : You got me there. We are, in fact doing something called “evil pairing”. If you want to mold the production code your way, you need to order it from by writing a test first. Let me show you how. Leela the universe very specific Chapter 5: Evil is good and negative is positive gameWithoutDependencies ; asyncFn ; describe( , () => { askPlayerToHitMock; messagePlayerMock; game; beforeEach( { askPlayerToHitMock = asyncFn(); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); game.encounterMonster(); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(askPlayerToHitMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( ); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.resolve( ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.resolve( ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); }); }); import from './monsterBeatdown' import from '@asyncFn/jest' 'given a monster is encountered' let let let => () askPlayerToHit messagePlayer 'informs the player of this' async 'You encounter a monster with 5 hit-points.' 'asks player to attack' 'Do you want to attack it?' // This is the "negation test" referred later in narrative. 'when player has not chosen anything yet, the player is not eaten' 'You chose to flee the monster, but the monster eats you in disappointment.' // This is another negation test. 'when player has not chosen anything yet, the game is not over' 'Game over.' 'when player chooses to flee' async await false 'player is informed of the grim outcome' 'You chose to flee the monster, but the monster eats you in disappointment.' 'the game is over' 'Game over.' 'when player chooses to attack' async await true // Another negation test 'player does not lose' 'You chose to flee the monster, but the monster eats you in disappointment.' 'the game is over' 'Game over.' Contents of so far: ./monsterBeatdown.js ({ messagePlayer, askPlayerToHit }) => ({ : () => { messagePlayer( ); playerChoseToAttack = askPlayerToHit( , ); (!playerChoseToAttack) { messagePlayer( , ); } messagePlayer( ); }, }); export default encounterMonster async 'You encounter a monster with 5 hit-points.' const await 'Do you want to attack it?' if 'You chose to flee the monster, but the monster eats you in disappointment.' 'Game over.' : There. I’ve reduced the “evilness” of the code by adding something we call “negation tests”. By writing tests that describe what is not supposed to happen we forced the production code to make a little bit more sense. And if you are wondering where the name “evil pairing” comes from, it comes from playing unit testing ping-pong in such a way that the production code is always written in the most evil way possible, ie. the code is non-sensical for real-life, yet it still satisfies the tests. The only way out of this is to force sense in production through unit tests. Sometimes you can tell coders are evil pairing from the . In general, this “ ” helps produce code that is very robust for the sake of refactoring, and also allows programmers to hone their a little bit more. But I digress. However important this may be, it is slightly off-course. What is relevant is that supports evil pairing as a line of thinking. Let’s motor on. Leela evil laughter evil pairing -mentality TDD-mojo asyncFn Chapter 6: The multiplicity challenge and the enlightenment : For sure. Can we see how the game develops some steps further? Particularly, I’ve been suffering from testing of functions that are called , yet return promises. Fry multiple times : I feel you. My guess is an asyncFn-mock will get called multiple times soon enough, when we start attacking the monster multiple times. Let’s see how that pans out. Leela gameWithoutDependencies ; asyncFn ; describe( , () => { askPlayerToHitMock; messagePlayerMock; game; beforeEach( { askPlayerToHitMock = asyncFn(); messagePlayerMock = jest.fn(); game = gameWithoutDependencies({ : askPlayerToHitMock, : messagePlayerMock, }); game.encounterMonster(); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(askPlayerToHitMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( ); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.resolve( ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); }); it( , () => { askPlayerToHitMock.resolve( ); expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); describe( , () => { beforeEach( () => { askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( , ); }); it( , () => { expect(messagePlayerMock).toHaveBeenCalledWith( ); }); it( , () => { expect(messagePlayerMock).not.toHaveBeenCalledWith( , ); }); }); it( , () => { askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); askPlayerToHitMock.resolve( ); expect(messagePlayerMock).not.toHaveBeenCalledWith( ); }); }); import from './monsterBeatdown' import from '@asyncFn/jest' 'given a monster is encountered' let let let => () askPlayerToHit messagePlayer 'informs the player of this' async 'You encounter a monster with 5 hit-points.' 'asks player to attack' 'Do you want to attack it?' 'when player has not chosen anything yet, the game is not lost' 'Game over.' 'when player chooses to flee' async await false 'player is informed of the grim outcome' 'You chose to flee the monster, but the monster eats you in disappointment.' 'the game is not won' 'You knock out the monster. You are a winner.' 'the game is over' 'Game over.' 'when player hits the monster, the monster loses a hit-point' async await true 'You hit the monster. It now has 4 hit-points remaining.' 'when player hits the monster enough times to knock it out' async // Here the monster is attacked multiple times, and therefore, askPlayerToHitMock is called multiple times, and then the test resolves the mock multiple times. // Using asyncFn, the logic remains the same: things that happen are written and observed in chronological order. await true await true await true await true await true 'does not show out-of-place message about no hit-points' 'You hit the monster. It now has 0 hit-points remaining.' 'player is congratulated' 'You knock out the monster. You are a winner.' 'game is won' 'Game over.' 'the game is not lost' 'You chose to flee the monster, but the monster eats you in disappointment.' 'when player hits the monster until it only has 1 hit-point remaining, the game is not over yet' async // Hit the monster only 4 times instead of 5. await true await true await true await true 'Game over.' The final form of : ./monsterBeatdown.js ({ askPlayerToHit, messagePlayer }) => ({ : () => { monsterHitPoints = ; messagePlayer( , ); ( askPlayerToHit( )) { monsterHitPoints--; (monsterHitPoints === ) { ; } messagePlayer( , ); } (monsterHitPoints === ) { messagePlayer( ); } { messagePlayer( , ); } messagePlayer( ); }, }); export default encounterMonster async let 5 `You encounter a monster with hit-points.` ${monsterHitPoints} while await 'Do you want to attack it?' if 0 break `You hit the monster. It now has hit-points remaining.` ${monsterHitPoints} if 0 'You knock out the monster. You are a winner.' else 'You chose to flee the monster, but the monster eats you in disappointment.' 'Game over.' : There. Now the monster gets hit multiple times, all while things still happen in a clear order. Our work is done here. Leela : Color me enlightened. This has changed my view of the world as a programmer and a human being. I shall make sacrifices in your honor and get an asyncFn-tattoo. My grandchildren will know your name. Fry Summary Having witnessed , we’ve seen how the of permitted troublesome tests, with awkward readability, to be refactored orderly as a story, with significantly better readability. the adventure late resolve asyncFn is something that we, as a team, have used for multiple years now. It has categorically changed the way we approach unit testing of asynchronous code in JavaScript. Which we so adore. asyncFn See more about asyncFn for or . jest sinon Who are we? asyncFn is lovingly crafted by at from . Your Pals Team: Igniter Houston Inc. Consulting We are a software development team of friends, with proven tradition in professional excellence. We specialize in holistic without sacrificing . rapid deployments quality Come say hi at , us, or check out the . We just might be available for hiring ;) Gitter email team itself Previously published at https://medium.com/houston-io/how-to-unit-test-asynchronous-code-for-javascript-in-2020-41c124be2552