web-dev-qa-db-fra.com

Tester avec React's Jest et Enzyme lorsque des clics simulés appellent une fonction qui appelle une promesse

  • React v15.1.0
  • Jest v12.1.1
  • Enzyme v2.3.0

J'essaie de comprendre comment tester un composant qui appelle une promesse dans une fonction invoquée par un clic. Je m'attendais à ce que la fonction runAllTicks() de Jest m'aide ici, mais elle ne semble pas tenir sa promesse.

Composant:

import React from 'react';
import Promise from 'bluebird';

function doSomethingWithAPromise() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, 50);
  });
}

export default class AsyncTest extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      promiseText: '',
      timeoutText: ''
    };

    this.setTextWithPromise = this.setTextWithPromise.bind(this);
    this.setTextWithTimeout = this.setTextWithTimeout.bind(this);
  }

  setTextWithPromise() {
    return doSomethingWithAPromise()
      .then(() => {
        this.setState({ promiseText: 'there is text!' });
      });
  }

  setTextWithTimeout() {
    setTimeout(() => {
      this.setState({ timeoutText: 'there is text!' });
    }, 50);
  }

  render() {
    return (
      <div>
        <div id="promiseText">{this.state.promiseText}</div>
        <button id="promiseBtn" onClick={this.setTextWithPromise}>Promise</button>
        <div id="timeoutText">{this.state.timeoutText}</div>
        <button id="timeoutBtn" onClick={this.setTextWithTimeout}>Timeout</button>
      </div>
    );
  }
}

Et les tests:

import AsyncTest from '../async';
import { shallow } from 'enzyme';
import React from 'react';

jest.unmock('../async');

describe('async-test.js', () => {
  let wrapper;

  beforeEach(() => {
    wrapper = shallow(<AsyncTest />);
  });

  // FAIL
  it('displays the promise text after click of the button', () => {
    wrapper.find('#promiseBtn').simulate('click');

    jest.runAllTicks();
    jest.runAllTimers();

    wrapper.update();

    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
  });

  // PASS
  it('displays the timeout text after click of the button', () => {
    wrapper.find('#timeoutBtn').simulate('click');

    jest.runAllTimers();

    wrapper.update();

    expect(wrapper.find('#timeoutText').text()).toEqual('there is text!');
  });
});
13
Caspar

Il n'y a pas grand-chose à attendre d'une manière ou d'une autre que la promesse se réalise avant de terminer le test. Il y a deux façons principales de le faire à partir de votre code que je peux voir.

  1. tester indépendamment que onClick et vos méthodes de promesse. Vérifiez donc que onClick appelle la fonction correcte, mais espionnez setTextWithPromise, déclenchant un clic et affirmant que setTextWithPromise a été appelé. Ensuite, vous pouvez également obtenir l'instance du composant et appeler cette méthode qui renvoie la promesse que vous pouvez attacher un gestionnaire et affirmer qu'il a fait la bonne chose.

  2. exposer un accessoire de rappel que vous pouvez transmettre qui est appelé lorsque la promesse se résout.

1
monastic-panic

Réponse mise à jour: en utilisant async/await conduit à un code plus propre. Ancien code ci-dessous.

J'ai réussi à résoudre ce problème en combinant les éléments suivants:

  • Se moquer de la promesse et la concrétiser immédiatement
  • Rendre le test asynchrone en marquant la fonction de test async
  • Après avoir simulé le clic, attendez la prochaine macrotask pour laisser le temps à la promesse de se résoudre

Dans votre exemple, cela pourrait ressembler à ceci:

// Mock the promise we're testing
global.doSomethingWithAPromise = () => Promise.resolve();

// Note that our test is an async function
it('displays the promise text after click of the button', async () => {
    wrapper.find('#promiseBtn').simulate('click');
    await tick();
    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
});

// Helper function returns a promise that resolves after all other promise mocks,
// even if they are chained like Promise.resolve().then(...)
// Technically: this is designed to resolve on the next macrotask
function tick() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  })
}

La update() d'Enzyme n'est ni suffisante ni nécessaire lors de l'utilisation de cette méthode, car les promesses ne se résolvent jamais dans le même tick qu'elles ont été créées - par conception. Pour une explication très détaillée de ce qui se passe ici, voir cette question .

Réponse originale: même logique mais légèrement moins jolie. Utilisez setImmediate de Node pour différer le test jusqu'au prochain tick, c'est-à-dire lorsque la promesse se résoudra. Appelez ensuite Jest's done pour terminer le test de manière asynchrone.

global.doSomethingWithAPromise = () => Promise.resolve({});

it('displays the promise text after click of the button', (done) => {
    wrapper.find('#promiseBtn').simulate('click');

  setImmediate( () => {
    expect(wrapper.find('#promiseText').text()).toEqual('there is text!');
    done();
  })
});

Ce n'est pas aussi agréable, car vous obtiendrez de gros rappels imbriqués si vous devez attendre plus d'une promesse.

39
Jonathan Stray