Abhinav Rai

Async — Await & React Promise testing

With the ES6 taking over the planet, its time for everyone to either know Async — Await or spend their life in callback hell.

Am assuming you know promises. If you don’t , give this blog a read. It covers all the basics and then come back here. Lets start with basics of Async Await.

We don’t want complex things in life. Asynchronous things are the exception as we want to utilise their power. Promises were a great way to make the asynchronous task more readable than callbacks with their then and catch notations. Async Await is even more cleaner and intuitive than this.

const add10 = (num) => {
  return num + 10
}
console.log(add10(5)) // 15
const add5 = async (num) => num + 5;
console.log(add5(5)) // Promise {<resolved>: 10}

The only difference between the above 2 functions is the async keyword. Async wraps the return value in the promise. Another example:

const processDataAsycn = async (num) => {  
   if(typeof num === 'number') {  
     return 2*num;  
   } else {  
     throw new Error('Something went wrong');  
   }  
 };  
 processDataAsycn(21).then((data) => {  
   console.log('Data from processDataAsycn() with async( When promise gets resolved ): ' + data);  
 }).catch((error) => {  
   console.log('Error from processDataAsycn() with async( When promise gets rejected ): ' + error);  
 });

Now lets come to await —

  1. it only works on the function with prepended async (async functions)
// async function body continues ...
let value = await promise;

The keyword await makes JavaScript wait until that promise settles and returns its result.

const getDataPromise = (num) => new Promise((resolve, reject) => {  
   setTimeout(() => {  
     (typeof num === 'number') ? resolve(num * 2) : reject('Input must be an number');  
   }, 2000);  
   });  
 const processDataAsycn = async () => {  
   let data = await getDataPromise(2);  
   data = await getDataPromise(data);  
   return data;  
 };  
 processDataAsycn().then((data) => {  
   console.log('Data from processDataAsycn() with async( When promise gets resolved ): ' + data);  
 }).catch((error) => {  
   console.log('Error from processDataAsycn() with async( When promise gets rejected ): ' + error);  
 });

The return value of 8 comes after 4 seconds in the above case.

Lets see the example of promise chaining.

fetch('/article/promise-chaining/user.json')
  .then(user => fetch(`https://api.github.com/users/${user.name}`))
  .then(response => response.json())
  .then(githubUser => new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, 3000);
  }))
  // triggers after 3 seconds
  .then(githubUser => alert(`Finished showing ${githubUser.name}`));

Converting the above to the async await example:

function appendImageAndThenRemove(timeInSeconds) {
  return new Promise(function(resolve, reject) {
    let img = document.createElement('img');
    img.src = githubUser.avatar_url;
    img.className = "promise-avatar-example";
    document.body.append(img);

    setTimeout(() => {
      img.remove();
      resolve(githubUser);
    }, timeInSeconds*1000);
  })
}

function async doSomeCrazyThingWithAPI(){
  let user = await fetch('/article/promise-chaining/user.json');
  let response = await fetch('https://api.github.com/users/${user.name}');
  let githubUser = await appendImageAndThenRemove(10);
  // Will wait for 10 seconds here
  alert(githubUser);
}

It makes the code clean. What exactly happening is the same what happens in promise. It only goes in then, when the promise is resolved. Similarly the code execution is halted at await. Its not halted technically as other things can use the cpu till this promise is resolved. So anything further down is not processed till promise is resolved.

Error

Lets say the await promise is rejected. Then the promise wrapping done by async will also result in rejected promise if this await rejected promise is not handled in try … catch.

async function f() {

  try {
    let response = await fetch('http://no-such-url');
  } catch(err) {
    alert(err); // TypeError: failed to fetch
  }
}

f(); // f() returns a resolved promise but if we remove try catch, it will result in rejected promise

Great read and much more things can be found here on async await here

How async await saved me in react to solve a problem in testing —

Problem: Have to test a component. The component makes a call to an internal function in componentDidMount. There is an imported module which makes an API call and returns a promise. The internal function uses this imported API module and sets state on promise resolve and does something else on promise reject.

Unknowns:

  • How to mock an external imported module with jest/enzyme?
  • In tests, How to wait till promise is resolved and then check for state?

Solution:

Great reads for finding the solution to 1st:

  1. https://medium.com/codeclan/mocking-es-and-commonjs-modules-with-jest-mock-37bbb552da43
  2. https://jestjs.io/docs/en/es6-class-mocks
  3. https://stackoverflow.com/questions/40465047/how-can-i-mock-an-es6-module-import-using-jest

Now, in tests:

The problem is that I need to wait till the mocked call returns. This component did mount is an async task and i only have to assert only when the mocked promise gets resolved.

Solutions:

  1. https://github.com/facebook/jest/issues/2157#issuecomment-279171856 — with async/await
  2. https://github.com/airbnb/enzyme/issues/346#issuecomment-304535773
class ExampleComponent extends React.Component {
    constructor() {
      super()
      this.state = {
        dataReady: false
      }
    }

    componentDidMount () {
      fetch('example.com/data.json').then(data => {
        this.setState({ dataReady: true })
      })
    }
    
    render () {
      if (this.state.dataReady) {
        return (<div> data is ready! </div>)
      } else {
        return (<div> spinner! </div>)
      }
    }
}

React class code for above problem is just above and the solution is below.

it('ensures the state is set', () => {
  const promise = Promise.resolve(mockData);
  sinon.stub(global, 'fetch', () => promise);

  const wrapper = mount(<ExampleComponent />);

  return promise.then(() => {
    expect(wrapper.state()).to.have.property('dataReady', true);

    wrapper.update();
  }).then(() => {
    expect(wrapper.text()).to.contain('data is ready');
  });
});

Whats happening in the solution?

We use sinon stub to replace that whenever fetch function is being called, replace it with

function() { return Promise.resolve(mockData)}

So when we mount the component in line 5, this promise then is also called. Then line 7 then is being called over the result of this. Although this is hack as (I think) there is not way apart from using this advantage of race condition as of now.

Another way I am using it,

test("bar", async () => {
    
  const userAuthenticatedPropMock = jest.fn();
  const location = {
        search: "?ticket=1234"
      };
  let component = await mount(
    <Router initialEntries={["/authenticate"]}>
      <Route
        path="/authenticate"
        render={() => (
          <Authenticate
            userAuthenticated={userAuthenticatedPropMock}
            location={location}
          />
        )}
      />
    </Router>
  );
});

I don’t know why its working as I am not able to get the return type of mount. But till the promise is not being resolved for componentDidMount, this execution stays at await. Anyone knows how this is working — please comment in the blog.

Buy me a coffee by clapping on the post. For every 10 claps, I gift myself a coffee. You may reach me at me@abhinavrai.com