Menggunakan Moq untuk mengejek metode asinkron untuk uji unit

180

Saya menguji metode untuk layanan yang melakukan APIpanggilan Web . Menggunakan normal HttpClientberfungsi dengan baik untuk pengujian unit jika saya juga menjalankan layanan web (terletak di proyek lain dalam solusi) secara lokal.

Namun ketika saya memeriksa perubahan saya, build server tidak akan memiliki akses ke layanan web sehingga pengujian akan gagal.

Saya telah menemukan cara mengatasi ini untuk pengujian unit saya dengan membuat IHttpClientantarmuka dan mengimplementasikan versi yang saya gunakan dalam aplikasi saya. Untuk pengujian unit, saya membuat versi tiruan lengkap dengan metode posting asinkron yang diejek. Di sinilah saya mengalami masalah. Saya ingin mengembalikan nilai OK HttpStatusResultuntuk tes khusus ini. Untuk tes serupa lainnya saya akan mengembalikan hasil yang buruk.

Tes akan berjalan tetapi tidak akan pernah selesai. Itu tergantung menunggu. Saya baru mengenal pemrograman asinkron, delegasi, dan Moq itu sendiri dan saya telah mencari SO dan google untuk sementara waktu mempelajari hal-hal baru tetapi saya masih belum bisa menyelesaikan masalah ini.

Inilah metode yang saya coba untuk menguji:

public async Task<bool> QueueNotificationAsync(IHttpClient client, Email email)
{
    // do stuff
    try
    {
        // The test hangs here, never returning
        HttpResponseMessage response = await client.PostAsync(uri, content);

        // more logic here
    }
    // more stuff
}

Inilah metode unit test saya:

[TestMethod]
public async Task QueueNotificationAsync_Completes_With_ValidEmail()
{
    Email email = new Email()
    {
        FromAddress = "[email protected]",
        ToAddress = "[email protected]",
        CCAddress = "[email protected]",
        BCCAddress = "[email protected]",
        Subject = "Hello",
        Body = "Hello World."
    };
    var mockClient = new Mock<IHttpClient>();
    mockClient.Setup(c => c.PostAsync(
        It.IsAny<Uri>(),
        It.IsAny<HttpContent>()
        )).Returns(() => new Task<HttpResponseMessage>(() => new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

    bool result = await _notificationRequestService.QueueNotificationAsync(mockClient.Object, email);

    Assert.IsTrue(result, "Queue failed.");
}

Apa yang saya lakukan salah?

Terima kasih untuk bantuannya.

vanella
sumber

Jawaban:

351

Anda sedang membuat tugas tetapi tidak pernah memulainya, jadi itu tidak pernah selesai. Namun, jangan hanya memulai tugas - alih-alih, ubah menggunakan Task.FromResult<TResult>yang akan memberi Anda tugas yang sudah selesai:

...
.Returns(Task.FromResult(new HttpResponseMessage(System.Net.HttpStatusCode.OK)));

Perhatikan bahwa Anda tidak akan menguji asynchrony yang sebenarnya dengan cara ini - jika Anda ingin melakukan itu, Anda perlu melakukan sedikit lebih banyak pekerjaan untuk membuat Task<T>yang dapat Anda kontrol dengan cara yang lebih halus ... tapi itu sesuatu untuk hari yang lain.

Anda mungkin juga ingin mempertimbangkan untuk menggunakan palsu IHttpClientdaripada mengejek semuanya - itu benar-benar tergantung pada seberapa sering Anda membutuhkannya.

Jon Skeet
sumber
2
Terima kasih banyak. Itu bekerja dengan baik. Saya pikir itu mungkin sesuatu yang sederhana yang tidak saya mengerti.
mvanella
2
Re: Fake IHttpClient, saya mempertimbangkan hal itu tetapi saya harus dapat mengembalikan HttpStatusCodes yang berbeda untuk tes yang berbeda berdasarkan perilaku yang diharapkan kembali dari API web, dan ini sepertinya memberi saya lebih banyak kontrol.
mvanella
3
@vanella: Ya, jadi Anda akan membuat palsu yang dapat mengembalikan apa pun yang Anda inginkan. Hanya sesuatu untuk dipikirkan.
Jon Skeet
134
Bagi siapa saja yang menemukan ini sekarang, Moq 4.2 memiliki ekstensi yang disebut ReturnsAysnc, yang melakukan hal ini.
Stuart Grassie
3
@legacybass Saya tidak dapat menemukan tautan ke dokumentasi apa pun untuk itu, meskipun API API mengatakan bahwa mereka dibuat melawan v4.2.1312.1622 yang dirilis hampir tepat setahun yang lalu. Lihat komit ini yang dibuat beberapa hari sebelum rilis itu. Mengapa dokumen API tidak diperbarui ...
Stuart Grassie
17

Rekomendasikan @Stuart jawaban Grassie di atas.

var moqCredentialMananger = new Mock<ICredentialManager>();
moqCredentialMananger
                    .Setup(x => x.GetCredentialsAsync(It.IsAny<string>()))
                    .ReturnsAsync(new Credentials() { .. .. .. });
DineshNS
sumber
1

Dengan Mock.Of<...>(...)untuk asyncmetode Anda dapat menggunakan Task.FromResult(...):

var client = Mock.Of<IHttpClient>(c => 
    c.PostAsync(It.IsAny<Uri>(), It.IsAny<HttpContent>()) == Task.FromResult(new HttpResponseMessage(HttpStatusCode.OK))
);
bside
sumber