Sinon: Menyelesaikan janji ES6 asli tidak memicu panggilan balik saat menggunakan pengatur waktu palsu

Dibuat pada 23 Apr 2015  ·  30Komentar  ·  Sumber: sinonjs/sinon

Dalam pengujian berikut, saya mengharapkan panggilan balik untuk janji yang diselesaikan untuk dipanggil saat berada di dalam pengujian. Rupanya, janji asli tidak memanggil panggilan balik secara sinkron, tetapi menjadwalkannya untuk dipanggil dengan cara yang mirip dengan setTimeout(callback, 0) . Namun, itu tidak benar-benar menggunakan setTimeout, jadi implementasi pewaktu palsu sinon tidak memicu panggilan balik saat memanggil tick() .

describe 'Promise', ->
  beforeEach ->
    <strong i="8">@clock</strong> = sinon.useFakeTimers()
  afterEach ->
    @clock.restore()
    console.log 'teardown'

  it "should invoke callback", ->
    p = new Promise (resolve, reject) ->
      console.log 'resolving'
      resolve(42)
      console.log 'resolved'
    p.then ->
      console.log 'callback'
    @clock.tick()
    console.log "test finished"

Saya mengharapkan keluaran ini:

resolved
callback
test finished
teardown

Sebagai gantinya saya mendapatkan ini:

resolved
test finished
teardown
callback

Callback dipanggil setelah pengujian selesai, jadi pernyataan berdasarkan apa yang terjadi di dalam callback gagal.

Tidak ada bedanya apakah janji diselesaikan sebelum atau setelah menelepon then() .

Komentar yang paling membantu

Satu-satunya hal yang Anda butuhkan adalah menunggu janji mikro untuk dieksekusi.
Jadi pendekatan berikut ini bekerja dengan sempurna:

const tick = async (ms) => {
  clock.tick(ms);
};

it("imitates promise fulfill when called once", async () => {
    new Promise(resolve => setTimeout(resolve, 400)).then(callback);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(1);
  });

Semua 30 komentar

Saya juga mengalami masalah ini

Saya tidak berpikir ini dapat diselesaikan, karena spesifikasi Promise tidak menggunakan setTimeout , tetapi menjadwalkan pekerjaan baru untuk dilakukan tepat setelah yang sekarang.

Sudahkah Anda mencoba mengembalikan janji dari tes? Sebagian besar pustaka pengujian mendukung janji sekarang, dan jika fungsi it() mengembalikan janji, pelari pengujian akan menunggu janji untuk diselesaikan atau ditolak sebelum mencatat pengujian selesai.

Ya, mengembalikan janji dari tes mungkin berhasil. Tetapi ketika menggunakan pengatur waktu palsu, saya berharap dapat bersaing dalam tes sebelum kembali dari fungsi tes. Itu adalah inti dari keseluruhannya.

Saya cukup yakin ini dapat dilakukan dengan mengganti objek Promise, sama seperti objek Date diganti. Untuk dapat melakukan itu, kita mungkin memerlukan implementasi yang bersaing dari spesifikasi janji, karena kita tidak akan dapat mengandalkan implementasi asli.

Saya melihat masalah yang kalian tunjukkan di sini. Saya ingin menyebutkan situasi spesifik yang saya hadapi, karena berdasarkan spesifikasi janji dan dokumentasi sinon, bagi saya tampaknya "harus" ini berfungsi. Dalam hal ini saya mengembalikan janji ke tes tetapi menggunakan penghitung waktu palsu tampaknya mencegah janji itu untuk diselesaikan.

Tampaknya bagi saya bahwa kedua tes ini harus berhasil, tetapi yang pertama lulus sementara yang kedua gagal. Saya menggunakan chai dengan plugin chai-as-promised dengan mocha dan saya mendapatkan kesalahan "batas waktu 2000 md terlampaui. Pastikan panggilan balik done() dipanggil dalam pengujian ini."

describe('chai as promised', function() {

    it('should resolve a promise', function() {
        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });
        return expect( promise ).to.have.been.fulfilled;
    });

});

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    after(function() {
        this.clock.restore();
    });

    it('should resolve a promise after ticking', function() {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });

        this.clock.tick( 1001 );

        return expect( promise ).to.have.been.fulfilled;
    });

});

Saya menyusun proyek uji cepat: https://github.com/JustinLivi/sinon-promises-test

Memulihkan sebelum penyelesaian tes tampaknya menjadi solusi yang layak:

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    it('should resolve a promise after ticking', function() {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 1000 );
        });

        this.clock.tick( 1001 );
        this.clock.restore();

        return expect( promise ).to.have.been.fulfilled;
    });

});

Solusi yang kami gunakan adalah mengganti implementasi janji asli dengan sesuatu yang sederhana yang bergantung pada setTimeout, saat menjalankan tes:

window.Promise = require('promise-polyfill')

Sinon.JS dapat melakukan hal yang sama saat memanggil useFakeTimers.

@JustinLivi memulihkan sebelum selesai melakukannya untuk saya :+1:

Jadi, ada solusi atau solusi?
Saya tidak dapat memodifikasi aplikasi saya dan berhenti menggunakan ES6 Promises, karena pengujian saya mengatakan demikian :)

Saya punya masalah, bukan dengan janji asli tetapi dengan polyfill es6-promise.
Solusi clock.restore() dari @JustinLivi memperbaiki masalah.

Tidak tahu mengapa masalah ini telah berlarut-larut di sini begitu lama, tapi ini bukan masalah dengan Sinon. Menggunakan Promises pada dasarnya melakukan logika async. Namun, semua tes di sini menggunakan logika sinkron (seperti yang memang disebutkan oleh OP). Jadi meskipun Anda memalsukan waktu, Anda masih mengandalkan Promise untuk mengeksekusi setelah fungsi Anda selesai, yang berarti Anda perlu sedikit mengubah pengujian Anda. Ini biasanya tercakup dalam dokumen sebagian besar kerangka kerja pengujian ( ini satu dari Mocha ), tetapi kami akan membahasnya dengan beberapa artikel yang menampilkan resep pengujian di situs baru yang akan datang.

Jadi mengubah contoh @JustinLivi sedikit, kita berakhir dengan tes berikut:

var sinon = require('sinon');

describe('sinon.useFakeTimers()', function() {

    before(function() {
        this.clock = sinon.useFakeTimers();
    });

    it('should resolve a promise after ticking', function(done) {

        var promise = new Promise(function( resolve ) {
            setTimeout( resolve, 10000 );
        });

        this.clock.tick( 10001 );

        promise
            .then(done) // call done when the promise completes
            .catch(done); // catch any accidental errors
    });

});

Sebenarnya, jika Anda menggunakan Mocha, ia memiliki versi "pintasan" dari kode yang sama di atas saat menggunakan Promises, jika Anda hanya mengembalikan janji langsung ke kerangka pengujian:

it('should resolve a promise after ticking', function() {
    var promise = new Promise(function( resolve ) {
        setTimeout( resolve, 10000 );
    });

    this.clock.tick( 10001 );

    return promise;
});

Ini akan

  1. Jalankan fungsi executor yang dikirim sebagai parameter ke konstruktor Promise secara sinkron (lihat spesifikasi ES2015 ), secara efektif menyiapkan batas waktu baru.
  2. Kembalikan janji yang dibangun.
  3. Centang waktu.
  4. Memicu fungsi batas waktu, menandai janji sebagai terselesaikan
  5. process (atau browser) akan menumpuk "centang" baru, menyelesaikan janji dan memanggil Thenables yang tersisa

Menutup ini sebagai non-bug.

@ fatso83 Saya tidak melihat bagaimana Anda mendapatkan tes untuk lulus seperti yang Anda gambarkan. Saya menambahkan tes contoh Anda ke repositori pengujian saya dan saya masih mendapatkan timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.

Lihat di sini: https://github.com/JustinLivi/sinon-promises-test/blob/master/test.js#L60
Juga, sejauh yang saya tahu, tes ini masih gagal: https://github.com/JustinLivi/Sinon.JS/commit/de106e6db2f5cc076b7e3a78635bd9ae2b6be1c2

Sunting: berdasarkan komentar oleh @fpirsch sepertinya hanya saat menggunakan polyfill janji es6? Adakah yang mencoba dengan perpustakaan alternatif seperti bluebird?

@fatso83 Ini BUKAN masalah return promise .
Dalam kasus saya ini adalah masalah Phantom (lama, tanpa janji) + es6-promises polyfill + sinon.js. Di browser modern, janji diselesaikan dan tes berjalan dengan baik, tetapi dengan janji polyfill, janji tidak pernah diselesaikan ketika penghitung waktu dipalsukan.

@JustinLivi dan @fpirsch : laporan bug ini tentang menggunakan janji _native_. Saya melihat proyek uji @JustinLivi menggunakan es6-promise , jadi saya tidak dapat menjaminnya. Hal yang sama berlaku untuk polyfill lainnya: itu akan menjadi masalah lain. Saya telah menguji ini dengan Node 5 dan 6, yang memiliki dukungan janji asli.

Copy-paste kode contoh saya dari posting sebelumnya dan menjalankannya (memiliki Mocha dan Sinon yang sudah diinstal sebelumnya):

$ pbpaste > test.js

$ mocha test.js

  sinon.useFakeTimers()
    ✓ should resolve a promise after ticking

  1 passing (12ms)

@fpirsch : Saya tidak pernah mengatakan itu adalah masalah return promise . Saya baru saja menyebutkan bahwa dia dapat menulis ulang tes dalam bentuk yang lebih pendek. Sebuah tip, bukan perbaikan. Tetapi Anda memang memberikan beberapa info berharga: ini berfungsi di browser asli, tetapi gagal di polyfill janji. Itu adalah kesalahan dari janjimu, bukan Sinon. Lib janji Anda kemungkinan besar tidak men-cache setTimeout dan teman, dan bergantung padanya untuk fungsi, sehingga melanggar. Saya menerapkan perbaikan untuk ini di lib pinkyswear sekali, jadi tidak heran.

Memeriksa sumber es6-promise dan tidak melakukan cache referensi ke setTimeout , jadi itu akan terpengaruh oleh apa pun yang dilakukan sinon. Tapi saya gagal melihat bagaimana penjadwal itu relevan di sini ... Ada juga daftar besar opsi lain yang akan dicoba polyfill sebelum kembali ke penjadwal batas waktu. Setiap browser yang mendarat di fallback itu harus kuno :)

Tapi bagaimanapun, apa yang @fpirsch dan @JustinLivi sebutkan adalah masalah mengenai interoperabilitas antara Sinon dan polyfill. Dan itu adalah masalah lain. Tidak melihat bagaimana Sinon dapat berbuat banyak tentang hal itu ATM (PR apa pun lebih cenderung berakhir di es6-polyfill ), tetapi jika ini adalah masalah Sinon jangan ragu untuk membuka masalah baru.

PS Saya melihat tip dari @ropez adalah menggunakan promise-polyfill , dan itu memang rekomendasi kami untuk polyfill minimal juga, tetapi untuk alasan yang berlawanan dari yang diberikan. @mroderick menambal proyek itu pada bulan Januari (beberapa bulan setelah komentar @ropez ) ke cache referensi ke setTimeout sehingga janji akan menyelesaikan _tidak peduli apa yang dilakukan sinon_ ke referensi global setTimeout . Lihat taylorhakes/promise-polyfill#15 untuk detailnya.

@fatso83 terima kasih atas infonya. Sayangnya menyimpan referensi ke setTimeout seperti yang dilakukan @mroderick untuk promise-polyfill tidak berfungsi dalam kasus saya. Juga tidak mengganti es6-promise dengan promise-polyfill :-(
EDIT: itu ada dalam tumpukan hantu + moka sederhana, tetapi tidak dalam tumpukan lengkap kami dengan karma. Sangat lelah dengan ini.

@fatso83 Saya memberikan contoh di sini: faketimers
Masalahnya mungkin dengan Karma setelah semua ... Saya akan mencoba untuk menyelidiki lebih dalam ini.

@fpirsch : Saya pikir Anda sedang melakukan sesuatu. Baik Anda dan @JustinLivi mengandalkan karma sebagai pelari uji. Mereka tampak sangat mirip. PS Saya menghapus komentar saya karena saya pikir itu benar-benar berlebihan, karena Justin telah memberikan kasus uji yang menunjukkan masalah tersebut, tetapi proyek contoh Anda memperkuat hipotesis bahwa ini adalah hal Karma, jadi terima kasih!

Ini mungkin terkait tetapi saya tidak yakin. Mengapa tes ketiga di bawah ini gagal? Kegagalan spesifik adalah batas waktu pengujian yang khas:

     Error: timeout of 2000ms exceeded. Ensure the done() callback is being called in this test.
describe.only('working', () => {
  let clock;
  beforeEach(() => { clock = Sinon.useFakeTimers(); })
  afterEach(() => { clock.restore() })

  const delay = (ms) => (
    new Promise((resolve/* , reject */) => {
      setTimeout(resolve, ms)
    })
  );

  it('1 OK', () => {
    const promise = new Promise((resolve) => {
      setTimeout(resolve, 100)
    })
    clock.tick(200)
    return promise
  })

  it('2 OK', () => {
    const promise = delay(100)
    clock.tick(200)
    return promise
  })

  it('3 FAIL', () => {
    const promise = delay(50).then(() => delay(50))
    clock.tick(200)
    return promise
  })
})

@jasonkuhrt Tes ketiga gagal karena clock.tick memproses eksekutor secara sinkron dan dengan demikian .then(...) tidak dieksekusi dalam karena sifatnya yang asinkron :

onFulfilled atau onRejected tidak boleh dipanggil hingga tumpukan konteks eksekusi hanya berisi kode platform.

@fatso83 Saya tidak tahu posisi pengembang lolex terkait janji berantai yang menambahkan penghitung waktu. Batasan saat ini setidaknya harus dirinci dalam dokumentasi.

Tergantung pada apakah ini harus didukung atau tidak, masalah ini dapat dibuka kembali (atau yang baru dapat dibuka) dan bergantung pada masalah baru di lolex .
Saya juga menduga bahwa jika kasus penggunaan ini (merantai janji yang menambahkan penghitung waktu) harus diperhitungkan, ini akan menyebabkan modifikasi API lolex yang mengganggu (AFAIK clock.tick harus asinkron).

@gautaz Ah masuk akal sekarang. Terima kasih telah menjelaskan!

lolex adalah perpustakaan sinkronisasi, dan saya tidak sepenuhnya yakin bagaimana menyelesaikan masalah @jasonkuhrt . Jangan ragu untuk memberikan PR, tetapi memodifikasi clock.tick tidak terlalu menarik. Saya lebih suka melihat metode async tambahan.

Saya tidak pernah memiliki banyak masalah dengan janji dan detak jam, tetapi itu mungkin karena saya telah melihat bahwa mereka sinkron, dan menambahkan kutu jam ekstra di antaranya.

@fatso83 Anda benar, menambahkan tick asinkron akan lebih sedikit mengganggu (sebenarnya tidak mengganggu sama sekali).
Jadi sesuatu seperti asyncTick mungkin cocok. Saya akan memeriksanya jika saya bisa meluangkan waktu.

@gautaz hanya ingin tahu apakah Anda pernah menyediakan patch ke lolex ?

@fatso83 Hai, saya sudah memulai cabang tahun lalu di pihak saya tetapi tidak pernah punya waktu untuk menyelesaikan pekerjaan saat ini.
Saya memiliki rekan kerja yang juga tersandung oleh masalah yang sama, jadi saya hanya bisa berharap masalah ini menjadi cukup besar untuk memberi saya waktu tambahan.

Apakah ada yang berubah sementara itu mengenai lolex API pada poin khusus ini?

Seharusnya tidak. Sangat sedikit perubahan dalam basis kode setengah tahun terakhir. Cukup stabil.

Adakah peluang untuk mendapatkan solusi sederhana di sini? Punya masalah yang sama. Saya punya dua janji dan itu diselesaikan oleh setTimeout. Saya perlu memeriksa hal-hal sebelum penyelesaian apa pun, sebelum penyelesaian kedua tetapi setelah penyelesaian pertama, dan setelah semua penyelesaian.

Satu-satunya hal yang Anda butuhkan adalah menunggu janji mikro untuk dieksekusi.
Jadi pendekatan berikut ini bekerja dengan sempurna:

const tick = async (ms) => {
  clock.tick(ms);
};

it("imitates promise fulfill when called once", async () => {
    new Promise(resolve => setTimeout(resolve, 400)).then(callback);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(0);

    await tick(200);
    expect(callback.callCount).to.eql(1);
  });

Trik @jakwuh berhasil untuk saya (Node 8, tanpa transpilasi, janji asli), kecuali panggilan balik then ditunda oleh tanda centang.

Komentar yang Diperbarui : Solusi dalam komentar asli saya (di bawah) memiliki masalah. Terkadang saya perlu melakukan beberapa panggilan await Promise.resolve() berturut-turut untuk benar-benar menghapus semuanya. Inilah sesuatu yang tampaknya bekerja sedikit lebih baik:

beforeEach(function() {
    const originalSetImmediate = setImmediate;
    this.clock = sinon.useFakeTimers();
    this.tickAsync = async ms => {
        this.clock.tick(ms);
        await new Promise(resolve => originalSetImmediate(resolve));
    }
});

afterEach(function() {
    this.clock.restore();
});

Komentar Asli [PERINGATAN: Tidak berfungsi sebaik kode di atas.]

Menambahkan celah asinkron tambahan ke pembantu tick memperbaiki masalah bagi saya:

```js
const centang = async (ms) => {
clock.tick(ms);
menunggu Janji.resolve();
};

Bagi orang yang tertarik dengan pekerjaan untuk meningkatkan Sinon di bidang ini (sebenarnya, proyek saudaranya lolex ), lihat diskusi di sini:

Apakah halaman ini membantu?
0 / 5 - 0 peringkat