Jest: Sediakan API untuk membersihkan antrean resolusi Promise

Dibuat pada 23 Nov 2016  ·  46Komentar  ·  Sumber: facebook/jest

Apakah Anda ingin meminta fitur atau melaporkan bug ?

_Feature_, saya kira, tetapi yang cukup penting ketika menguji kode yang menggunakan Promise s.

Apa perilaku saat ini?

Saya memiliki komponen yang menggunakan pembungkus dan rantai Promise internal sebagai tindak lanjut dari tindakan asinkron eksternal. Saya memberikan tiruan dari tindakan async dan menyelesaikan janji yang dikembalikan dalam pengujian saya.

Komponennya seperti ini:

class Component extends React.Component {
  // ...
  load() {
    Promise.resolve(this.props.load())
      .then(
        result => result
          ? result
          : Promise.reject(/* ... */)
        () => Promise.reject(/* ... */)
      )
      .then(result => this.props.afterLoad(result));
  }
}

Dan kode pengujian terlihat seperti ini:

const load = jest.fn(() => new Promise(succeed => load.succeed = succeed));
const afterLoad = jest.fn();
const result = 'mock result';
mount(<Component load={load} afterLoad={afterLoad} />);
// ... some interaction that requires the `load`
load.succeed(result);
expect(afterLoad).toHaveBeenCalledWith(result);

Tes gagal karena expect() dievaluasi sebelum penangan janji yang dirantai. Saya harus mereplikasi panjang rantai janji batin dalam pengujian untuk mendapatkan apa yang saya butuhkan, seperti ini:

return Promise.resolve(load.succeed(result))
  // length of the `.then()` chain needs to be at least as long as in the tested code
  .then(() => {})
  .then(() => expect(result).toHaveBeenCalledWith(result));

Apa perilaku yang diharapkan?

Saya berharap Jest menyediakan semacam API untuk menghapus semua penangan janji yang tertunda, misalnya:

load.succeed(result);
jest.flushAllPromises();
expect(result).toHaveBeenCalledWith(result);

Saya sudah mencoba runAllTicks dan runAllTimers tidak berpengaruh.


_Atau, jika saya kehilangan beberapa fitur atau pola yang sudah ada, saya berharap seseorang di sini mengarahkan saya ke arah yang benar :)_

Enhancement New API proposal

Komentar yang paling membantu

Fungsi pembantu dapat mengubahnya menjadi janji itu sendiri sehingga Anda tidak perlu berurusan dengan panggilan balik yang sudah selesai. Ini cukup kecil sehingga tidak berbahaya untuk disimpan di userland, tapi saya tidak akan mengeluh jika itu diletakkan di objek lelucon. Sesuatu seperti ini banyak digunakan dalam proyek saya.

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

Dengan async menunggu itu hampir cantik:

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

Semua 46 komentar

Meskipun pengujian async menjanjikan, ada baiknya untuk diingat bahwa Anda dapat mengembalikan fungsi pengujian sebagai Janji, jadi sesuatu seperti ini akan berfungsi:

test('my promise test', () => { //a test function returning a Promise
  return Promise.resolve(load.succeed(result))
    .then(() => {})
    .then(() => expect(result).toHaveBeenCalledWith(result));
})

Mengembalikan Janji dari fungsi pengujian membuat Jest sadar bahwa ini adalah pengujian asinkron dan menunggu hingga diselesaikan atau waktu habis.

@thymikee Tentu saja saya mengembalikan nilai untuk membuat Jest menunggu - itu benar-benar tidak penting. Perhatikan bagaimana Anda bahkan meninggalkan baris .then(() => {}) dalam kode Anda. Saya tidak melihat bagaimana saya bisa menjelaskan masalah lebih ringkas daripada yang sudah saya lakukan di posting pembuka. Harap baca dengan seksama dan buka kembali masalah atau jelaskan cara mengatasinya.

_Saya telah menambahkan return ke kode di OP untuk menghindari kebingungan._

Mengalami masalah serupa, dan menjelaskannya di sini: https://github.com/pekala/test-problem-example

Singkatnya: Saya mencoba untuk menegaskan urutan tindakan yang dikirim ke toko Redux sebagai hasil dari interaksi pengguna (disimulasikan menggunakan enzim). Tindakan sebagai sinkronisasi terkirim dan asinkron menggunakan Janji (diolok-olok untuk diselesaikan segera). Tampaknya tidak ada cara untuk menegaskan setelah rantai Promise habis, jika Anda tidak memiliki akses langsung ke rantai janji. setTimeout(..., 0) berfungsi, tetapi terasa seperti peretasan dan jika pernyataan dalam panggilan balik setTimeout gagal, Jest gagal dengan kesalahan batas waktu (bukan kesalahan pernyataan).

Ide flushAllPromises sepertinya merupakan solusi, meskipun menurut saya itulah yang harus dilakukan runAllTicks ?

Sebagai tindak lanjut: Saya mencoba mengganti setTimeout(..., 0) dengan setImmediate dan ini tampaknya menjalankan pernyataan setelah antrian microtask panggilan balik Promise habis dan mencegah Jest dari waktu habis pada kesalahan pernyataan. Jadi, ini berfungsi dengan baik, dan merupakan solusi yang dapat diterima untuk kasus penggunaan saya:

test('changing the reddit downloads posts', done => {
    setImmediate(() => {
        // assertions...
        done()
    })
})

Fungsi pembantu dapat mengubahnya menjadi janji itu sendiri sehingga Anda tidak perlu berurusan dengan panggilan balik yang sudah selesai. Ini cukup kecil sehingga tidak berbahaya untuk disimpan di userland, tapi saya tidak akan mengeluh jika itu diletakkan di objek lelucon. Sesuatu seperti ini banyak digunakan dalam proyek saya.

function flushPromises() {
  return new Promise(resolve => setImmediate(resolve));
}

test('', () => {
  somethingThatKicksOffPromiseChain();
  return flushPromises().then(() => {
    expect(...
  });
})

Dengan async menunggu itu hampir cantik:

test('', async () => {
  somethingThatKicksOffPromiseChain();
  await flushPromises();
  expect(...
})

@jwbay itu gula yang enak di sana !

Memang benar bahwa flushPromises ternyata menjadi satu baris, tetapi sama sekali tidak jelas bagaimana menuju ke satu baris itu. Jadi saya pikir akan bermanfaat bagi pengguna Jest untuk menyediakannya sebagai fungsi util.

@pekala the one liner IMO tidak memberikan perilaku yang diperlukan karena tidak akan menunggu sampai janji tertunda berikut diselesaikan:

function foo() {  
  return new Promise((res) => {
    setTimeout(() => {
      res()
    }, 2000);
  });
}

Bagaimana dengan Promise swizzling dan ketika Promise baru dibuat tambahkan ke beberapa array kemudian flush semua janji akan menunggu di Promise.all di array ini?

@talkol Saya pikir itu akan terjadi, selama Anda juga menggunakan pengatur waktu palsu. Saya belum menguji itu.

@pekala tidak perlu memalsukan pengatur waktu dengan contoh ini karena janji akan diselesaikan hanya setelah waktu tercapai
Saya hanya khawatir Janji yang membingungkan akan mengacaukan pekerjaan batin yang bercanda, itu agak sulit

Jika Anda tidak memalsukan penghitung waktu, tes Anda akan membutuhkan waktu 2 detik+ untuk diselesaikan. Saya pikir praktik terbaik adalah menghapus jenis penundaan ini, dalam hal ini flushPromises seperti yang diusulkan oleh @jwbay berhasil .

Semua tergantung pada apa yang Anda coba uji :) Yang saya katakan adalah penghitung waktu adalah masalah yang tidak terkait dengan menunggu janji

Kami menghadapi masalah yang terkait dengan Janji yang tidak terselesaikan, yang bercampur dengan panggilan setTimeout. Di jest v19.0.2 kami tidak memiliki masalah, tetapi di jest v20.0.0 Janji tidak pernah masuk ke fungsi penyelesaian/tolak dan tes gagal. Masalah kami tampaknya terkait dengan masalah tidak memiliki _an API untuk menghapus antrian resolusi Janji_, tetapi masalah ini tampaknya mendahului lelucon v20.0.0 di mana kami mulai melihat masalah, jadi saya tidak sepenuhnya yakin.

Ini satu-satunya solusi yang dapat kami temukan untuk beberapa pengujian kami, karena kami memiliki serangkaian setTimeout s dan Promise yang digunakan dalam kode yang akhirnya memanggil onUpdateFailed panggilan balik.

  ReactTestUtils.Simulate.submit(form);
  return Promise.resolve()
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => { jest.runOnlyPendingTimers(); })
    .then(() => {
      expect(onUpdateFailed).toHaveBeenCalledTimes(1);
      expect(getErrorMessage(page)).toEqual('Input is invalid.');
    });

Tidak begitu cantik, jadi saran apa pun di sini sangat dihargai.

Contoh lain di mana Anda tidak dapat mengembalikan janji dari pengujian:

describe('stream from promise', () => {
  it('should wait till promise resolves', () => {
    const stream = Observable.fromPromise(Promise.resolve('foo'));
    const results = [];
    stream.subscribe(data => { results.push(data); });
    jest.runAllTimers();
    expect(results).toEqual(['foo']);
  });
});

Tes ini gagal dengan lelucon 20.0.4.

Solusi @philwhln juga dapat ditulis dengan async/menunggu

ReactTestUtils.Simulate.submit(form);

await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();
await jest.runOnlyPendingTimers();

expect(onUpdateFailed).toHaveBeenCalledTimes(1);
expect(getErrorMessage(page)).toEqual('Input is invalid.');

Saya akan menyukai fungsi utilitas yang menghapus antrian janji

Saya akan menyukai fungsi yang menghapus antrian janji di antara tes juga.

Saya sedang menguji kode yang menggunakan Promise.all untuk membungkus banyak janji. Ketika salah satu dari janji-janji yang dibungkus itu gagal (karena itulah yang ingin saya uji) janji itu segera kembali yang berarti janji-janji lain kadang-kadang (kondisi balapan, non deterministik) kembali saat tes berikutnya sedang berjalan.

Ini menyebabkan segala macam kekacauan dengan pengujian saya yang memiliki hasil yang tidak dapat diprediksi/dapat diulang.

Untuk menerapkan ini dengan benar, kita perlu mengejek Promise sehingga pada akhirnya kita dapat melihat semua tugas mikro yang diantrekan untuk menyelesaikannya secara sinkron. Sesuatu yang menghalangi apa yang dilakukan oleh tiruan janji .

Sudah ada API untuk menyiram tugas mikro yang diantrekan dengan process.nextTick dan API itu mungkin juga bekerja dengan Promises ( jest.runAllTicks ).

Saya punya solusi dengan melati yang terhubung ke nextTick of Yaku, perpustakaan janji dan menangkap panggilan nextTick dan memungkinkan memainkannya lebih awal.
Namun lelucon menggunakan janji itu sendiri, yang membuat ini bermasalah.
Pada akhirnya saya mengambil Yaku dan meretasnya untuk memiliki metode flush yang menghapus antriannya. Secara default ini berjalan secara normal menggunakan nextTick, tetapi jika Anda memanggil flush, semua penangan janji yang tertunda akan dieksekusi.
Sumbernya ada di sini:
https://github.com/lukeapage/yaku-mock
Itu bisa dilakukan dengan merapikan, menghubungi ysmood untuk melihat apa yang mereka pikirkan dan menambahkan dokumentasi, tetapi cukup banyak melakukan apa yang Anda inginkan dan bekerja untuk saya sebagai solusi sederhana untuk menyinkronkan janji dalam pengujian.

Sebagai solusi sederhana untuk itu, saya suka solusi @jwbay .

Bagaimana kalau kita menambahkan sesuatu yang mirip dengan objek jest ?

await jest.nextTick();

Diimplementasikan sebagai

const nextTick = () => new Promise(res => process.nextTick(res));

cc @cpojer @SimenB @rogeliog

Saya menggunakan enzim untuk memasang komponen React.

Saya juga memiliki fungsi yang mengharapkan Promises untuk dieksekusi, tetapi tidak ada perbaikan yang berhasil. Saya akan dapat menanganinya secara sinkron dalam pengujian saya - jika - fungsi mengembalikan objek Promise, menggunakan await , tetapi sayangnya fungsinya tidak mengembalikan objek Promise.

Ini adalah solusi yang akhirnya saya lakukan menggunakan mata-mata pada fungsi Janji global.

global.Promise = require.requireActual('promise');

it('my test', async () => {
    const spy = sinon.spy(global, 'Promise');

    wrapper.props().dispatch(functionWithPromiseCalls());

    for (let i = 0; i < spy.callCount; i += 1) {
      const promise = spy.getCall(i);
      await promise.returnValue;
    }

    expect(...)
});

Saya menemukan kasus penggunaan untuk ini (terima kasih @jwbay untuk teknik yang luar biasa)

Misalnya, Anda ingin memeriksa apakah fungsi Anda memiliki batas waktu, dan batas waktu diterapkan dengan tepat:

      jest.useFakeTimers();
      const EXPECTED_DEFAULT_TIMEOUT_MS = 10000;

      const catchHandler = jest.fn().mockImplementationOnce(err => {
        expect(err).not.toBeNull();
        expect(err.message).toContain('timeout');
      });

      // launch the async func returning a promise
      fetchStuffWithTimeout().catch(catchHandler);

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(EXPECTED_DEFAULT_TIMEOUT_MS - 1);
      await flushPromises();

      expect(catchHandler).not.toHaveBeenCalled(); // not yet

      jest.advanceTimersByTime(1);
      await flushPromises();

      expect(catchHandler).toHaveBeenCalledTimes(1); // ok, rejected precisely

mengembalikan janji tidak memungkinkan memeriksa waktu yang tepat dari resolusi/penolakan.

Pembilasan janji diperlukan di sana. Tanpa itu, ekspektasi disebut terlalu dini.

Semoga ini bisa membantu mempersempit masalah.

Untuk orang-orang yang mengikuti, ada PR terbuka untuk ini di sini: #6876

Pengeposan silang dari https://github.com/airbnb/enzyme/issues/1587

Saya ingin tahu apakah pola berikut ini cukup untuk menyelesaikan masalah ini, dan apakah saya melakukan sesuatu yang dianggap praktik buruk dan seharusnya tidak saya lakukan.

Apa yang orang pikirkan tentang pendekatan ini?

export class MyComponent extends React.Component {
  constructor (props) {
    super(props)

    this.hasFinishedAsync = new Promise((resolve, reject) => {
      this.finishedAsyncResolve = resolve
    })
  }

  componentDidMount () {
    this.doSomethingAsync()
  }

  async doSomethingAsync () {
    try {
      actuallyDoAsync()
      this.props.callback()
      this.finishedAsyncResolve('success')
    } catch (error) {
      this.props.callback()
      this.finishedAsyncResolve('error')
    }
  }

  // the rest of the component
}

Dan dalam tes:

it(`should properly await for async code to finish`, () => {
  const mockCallback = jest.fn()
  const wrapper = shallow(<MyComponent callback={mockCallback}/>)

  expect(mockCallback.mock.calls.length).toBe(0)

  await wrapper.instance().hasFinishedAsync

  expect(mockCallback.mock.calls.length).toBe(1)
})

Saya memiliki masalah ketika panggilan async tidak dilakukan langsung di componentDidMount, tetapi itu memanggil fungsi async, yang memanggil fungsi async lain dan seterusnya. Jika saya menambahkan langkah async tambahan di semua rantai async, saya perlu menambahkan tambahan .then() atau tambahan await , tetapi ini berfungsi dengan baik.

Apakah ada alasan mengapa saya tidak boleh menggunakan pendekatan ini atau apakah ini terlihat bagus bagi orang-orang?

Saya melakukan petualangan dalam melakukan ini di userland dan menemukan itu sebenarnya bisa dilakukan dan tidak terlalu buruk (walaupun ada beberapa jebakan yang harus dihadapi jika Anda tidak memiliki peta). Berikut adalah laporan pengalaman yang (semoga) cukup detail untuk digunakan secara langsung ; TLDR adalah untuk mentranspile async / await ke janji, dan menukar janji asli untuk bluebird dan timer asli untuk lolex; transpile semuanya , termasuk node_modules/ ; queueMicrotask adalah primitif yang Anda butuhkan untuk janji, tetapi secara default lolex tidak akan menyediakannya karena JSDOM tidak menyediakannya.

Saya mengalami masalah yang sama dengan jest.mockAllTimers() dan React komponen yang memanggil Promise di componentDidMount() .

Solusi dari #issuecomment-279171856 memecahkan masalah dengan cara yang elegan.

Kami membutuhkan sesuatu yang serupa di API Jest resmi!

Saya baru-baru ini mengalami masalah saat memutakhirkan banyak hal, itu mengungkapkan masalah dalam banyak tes di mana kami tidak selalu menunggu janji untuk diselesaikan. Dan sementara metode seperti await new Promise(resolve => setImmediate(resolve)); berhasil dalam kasus sederhana, saya menemukan dalam pengujian saya, saya harus menjalankannya beberapa kali untuk membersihkan pipa. Itulah yang disebutkan oleh @quasicomputational dalam eksplorasi mereka di sini . Sayangnya, saya tidak berpikir ada cara untuk mengetahui kapan pipa itu jelas tanpa mencegat janji saat dibuat. Jadi saya membuat perpustakaan kecil untuk melakukan itu... promise-spy . Padahal, saya memiliki satu tes yang menggunakan pengatur waktu palsu dan tidak berhasil dengan itu ... jadi ini belum merupakan solusi yang berfungsi sepenuhnya.

Meskipun saya juga membayangkan mereka hanya dapat bekerja dengan async / await semua dalam kode Anda untuk diuji JIKA mereka diubah menjadi janji. Jika mereka tidak diubah menjadi janji, maka perpustakaan ini tidak akan dapat menghubungkannya dan menunggu sampai mereka selesai.

Saya mendapati diri saya mengalami masalah yang sama dan saya menyadari:
kita seharusnya tidak menghapus janji yang tertunda tetapi sebaliknya kita harus membuat seluruh tes gagal jika ada janji yang tertunda.
Dengan cara ini kita akan dipaksa untuk membatalkan janji yang tertunda di dalam kode yang diuji menggunakan Abort Controller:
https://developers.google.com/web/updates/2017/09/abortable-fetch
Memiliki janji-janji pembilasan lelucon sama dengan mengatakan "Konkurensi itu sulit jadi jangan mengujinya". Pada kenyataannya seharusnya justru sebaliknya.
Karena konkurensi itu sulit, kita harus mengujinya lebih jauh dan sama sekali tidak mengizinkan tes untuk lulus dengan janji yang tertunda.

Mengingat kekacauan dalam membatalkan janji pada pertanyaan Stackoverflow ini jelas bukan (BELUM) hal yang mudah dilakukan:
https://stackoverflow.com/a/53933849/373542
Saya akan mencoba menulis implementasi KISS untuk membatalkan janji pengambilan saya dan akan memposting hasilnya di sini.

@giorgio-zamparelli: _"Konkurensi itu sulit jadi jangan mengujinya"_ benar-benar di luar inti dari laporan asli. Masalahnya tidak berkaitan dengan janji _pending_ melainkan dengan fakta bahwa menunggu propagasi janji _resolusi_ melalui kode async dalam pengujian tidak perlu sulit.

Saya pikir janji pembilasan akan menyembuhkan gejalanya, bukan penyakitnya.

Janji harus diselesaikan secara normal dalam pengujian tanpa perlu dihapus.
Jika ada janji yang tertunda dalam pengujian Anda, Anda harus menunggu untuk menyelesaikannya menggunakan misalnya wait dari @testing-library/react ATAU jika janji yang tertunda bukan bagian dari ruang lingkup pengujian, Anda juga harus mengejek kode yang memulainya atau Anda harus membatalkan janji yang tertunda di suatu tempat seperti pada acara siklus hidup React willUnmount menggunakan AbortController

AbortController adalah API baru yang hampir tidak digunakan oleh siapa pun dan saya merasa ini adalah perbaikan untuk sebagian besar janji yang menggantung dalam pengujian.

BUKTIKAN BAHWA AKU SALAH:
Saya dapat dengan mudah terbukti salah jika seseorang yang melaporkan mengalami masalah dengan masalah yang tertunda dalam Masalah ini sudah mencoba menggunakan AbortController dan jest.mock dan itu tidak cukup.

@giorgio-zamparelli: Mungkin kesalahpahaman berasal dari penggunaan frasa _"siram semua penangan janji yang tertunda"_ (dan jika ya, saya minta maaf). Seperti yang mudah-mudahan Anda akan lihat jika Anda membaca deskripsi masalah secara menyeluruh, maksud saya "penangan janji yang tertunda".

Jadi, untuk mengulangi, kita tidak berbicara tentang _pending_ janji di sini (dengan cara apapun), melainkan tentang pembilasan resolusi janji dengan kerumitan minimal. Atau, dengan kata lain, tentang mendapatkan secara transparan dan deterministik dari titik di mana janji diselesaikan ke titik di mana semua efek selanjutnya yang terkait dengannya dipanggil (sehingga kita dapat menguji hasilnya).

Saya baru-baru ini merilis flush-microtasks untuk tujuan ini. Itu meminjam implementasinya dari React, yang secara mengejutkan lebih kompleks daripada solusi @jwbay di sini atau solusi @thymikee di sini . Saya tidak yakin apakah kerumitannya membuat perbedaan yang berarti, tetapi saya menganggap itu menyumbang kasus Edge yang tidak dipertimbangkan oleh solusi lain di utas ini. Saya hanya menggunakan implementasi itu karena react-testing-library menggunakannya (lihat di sini ), tetapi tidak mengeksposnya.

import { flushMicroTasks } from 'flush-microtasks'

await flushMicroTasks()

@aleclarson Apakah ada perbedaan antara flush-microtasks dan flush-promises

@ramusus Sepertinya flush-promises menggunakan pendekatan yang sama dengan solusi @jwbay .

https://github.com/kentor/flush-promises/blob/46f58770b14fb74ce1ff27da00837c7e722b9d06/index.js

RTL juga telah menyalin kode React: https://github.com/testing-library/react-testing-library/blob/8db62fee6303d16e0d5c933ec1fab5841dd2109b/src/flush-microtasks.js

EDIT: hah, sudah disebutkan : nyengir:

Saya tidak yakin kita perlu membuatnya menjadi Jest ketika orang bisa menggunakannya? Mungkin kita bisa menautkannya di dokumen? Masalah ini adalah tentang membilasnya secara serempak, yang menurut saya melampaui apa yang ingin kami lakukan (terutama karena tidak mungkin dengan async-await )

Solusi flushPromises hanya berfungsi pada Janji yang segera diselesaikan tetapi tidak pada Janji yang masih tertunda.

Hmm, poin yang bagus. Saya tidak tahu apakah mungkin melacak janji pending entah bagaimana. Mungkin bisa melakukan sesuatu yang pintar dengan async_hooks , tidak yakin. Mungkin akan menyakitkan mencoba membedakan antara janji yang dibuat oleh kode userland dan janji yang dibuat oleh Jest dan dependensinya

Saya sudah mencoba untuk membungkus/mengolok-olok objek Promise untuk memasukkan penghitung tetapi itu tidak berhasil:

const _promise = window.Promise;
window.Promise = function(promiseFunction){
    // counter
    return new _promise(promiseFunction);
}

Masalah utama adalah fungsi async yang tidak menggunakan Promise global sama sekali

Ok, saya telah menemukan cara yang benar-benar hacky seperti ini .

  1. Buat modul baru dengan daftar.
  2. Tambahkan Janji Anda ke daftar itu.
  3. Selesaikan Janji dalam pengujian Anda dan hapus dari daftar.
  4. Dalam kasus saya, saya menjalankan wrapper.update() dari enzim. Lakukan di sini sesuatu yang serupa jika perlu.
  5. Ulangi Langkah 3 & 4 sampai daftar kosong.

Saya tahu, ini bukan praktik yang baik untuk menyesuaikan kode dengan tes TAPI saya sudah menggunakan logika ini pada rendering sisi server. Tapi pada akhirnya hanya menunggu. \_(ツ)_/¯

Ada pembaruan menarik untuk ini di Jest 26, di mana pengatur waktu palsu sekarang didasarkan pada @sinon/fake-timers (jika diaktifkan dengan jest.useFakeTimers('modern') ).

Saya mencoba penghitung waktu palsu modern dengan pengujian saya, dan sayangnya itu menyebabkan peretasan await new Promise(resolve => setImmediate(resolve)); hang tanpa batas. Untungnya, @sinon/fake-timers menyertakan beberapa metode *Async() yang "juga akan memutus loop acara, memungkinkan setiap panggilan balik janji terjadwal untuk mengeksekusi _before_ menjalankan timer.". Sayangnya, saya tidak melihat cara apa pun untuk mendapatkan objek clock melalui API Jest.

Adakah yang tahu cara membuat Jest memberi kami objek clock itu?

Seperti yang lain, motivasi saya untuk menggunakan await new Promise(setImmediate); adalah untuk menghapus janji yang dapat diselesaikan, sehingga saya dapat menguji unit dampaknya pada sistem.

Tampaknya pengatur waktu palsu "modern" memang berkinerja buruk dengan yang lain dengan mengatur waktu yang tampaknya tidak masuk akal.

Berikut beberapa unit test untuk menggambarkan ini:

describe('flushing of js-queues using different timers', () => {
  beforeAll(() => {
    // It would take the failing test 5 long seconds to time out.
    jest.setTimeout(100);
  });

  it.each([
    [
      'given real timers',
      () => {
        jest.useRealTimers();
      },
    ],
    ['given no timers', () => {}],
    [
      'given "legacy" fake timers',
      () => {
        jest.useFakeTimers('legacy');
      },
    ],
    [
      // This is the the failing scenario, not working like the other timers.
      'given "modern" fake timers',
      () => {
        jest.useFakeTimers('modern');
      },
    ],
  ])(
    '%s, when using setImmediate to flush, flushes a promise without timing out',
    async (_, initializeScenarioSpecificTimers) => {
      initializeScenarioSpecificTimers();

      let promiseIsFlushed = false;

      Promise.resolve().then(() => {
        promiseIsFlushed = true;
      });

      // Flush promises
      await new Promise(setImmediate);

      expect(promiseIsFlushed).toBe(true);
    },
  );
});

Saya merasa seperti tes sebelumnya tidak boleh gagal seperti itu.

Bagi saya solusinya adalah menyiram janji dengan menggunakan "setImmediate" asli-simpul dari "pengatur waktu" paket, alih-alih "setImmediate" global. Setelah ini, berikut ini berlalu:

import { setImmediate as flushMicroTasks } from 'timers';

it('given "modern" fake timers, when using native timers to flush, flushes a promise without timing out', async () => {
  jest.useFakeTimers('modern');

  let promiseIsFlushed = false;

  Promise.resolve().then(() => {
    promiseIsFlushed = true;
  });

  // Flush micro and macro -tasks
  await new Promise(flushMicroTasks);

  expect(promiseIsFlushed).toBe(true);
});

Terima kasih @aleclarson.

Inilah solusi kami untuk masalah ini:

https://github.com/team-igniter-from-houston-inc/async-fn
https://medium.com/houston-io/how-to-unit-test-asynchronous-code-for-javascript-in-2020-41c124be2552

Kode tes dapat ditulis seperti:

// Note: asyncFn(), extends jest.fn() with a way to control resolving/rejecting of a promise
const load = asyncFn();

const afterLoad = jest.fn();
const result = 'mock result';

mount(<Component load={load} afterLoad={afterLoad} />);

// ... some interaction that requires the `load`

// Note: New way to controlling when promise resolves
await load.resolve(result);

expect(afterLoad).toHaveBeenCalledWith(result);

Perhatikan bagaimana Anda tidak perlu tahu apa-apa tentang membuang janji atau menjalankan pengatur waktu.

@jansav bagus/+1. Fwiw Saya telah melihat pendekatan yang disebut pola yang ditangguhkan. Saya pikir itu membuat tes yang lebih bagus.

Sepertinya saya bahwa masalah dengan timer palsu adalah bahwa hal itu merusak run-loop alami untuk bagaimana timer seharusnya berfungsi. Saya bertanya-tanya mengapa kita tidak bisa membuat fungsi run timer lelucon menjadi async? Mengubah penghitung waktu untuk diselesaikan secara sinkron memang membuat kode pengujian terlihat rapi, tetapi menyebabkan efek samping yang sangat besar ini.

kasus penggunaan saya:

public static resolvingPromise<T>(result: T, delay: number = 5): Promise<T> {
    return new Promise((resolve) => {
        setTimeout(
            () => {
                resolve(result);
            },
            delay
        );
    });
}

berkas tes:

it("accepts delay as second parameter", async () => {
    const spy = jest.fn();
    MockMiddleware.resolvingPromise({ mock: true }, 50).then(spy);
    jest.advanceTimersByTime(49);
    expect(spy).not.toHaveBeenCalled();
    jest.advanceTimersByTime(1);
    await Promise.resolve(); // without this line, this test won't pass
    expect(spy).toHaveBeenCalled();
});
Apakah halaman ini membantu?
0 / 5 - 0 peringkat

Masalah terkait

Antho2407 picture Antho2407  ·  3Komentar

kgowru picture kgowru  ·  3Komentar

StephanBijzitter picture StephanBijzitter  ·  3Komentar

samzhang111 picture samzhang111  ·  3Komentar

jardakotesovec picture jardakotesovec  ·  3Komentar