Moq: Cara mendapatkan parameter yang diteruskan ke metode layanan yang diejek

169

Bayangkan kelas ini

public class Foo {

    private Handler _h;

    public Foo(Handler h)
    {
        _h = h;
    }

    public void Bar(int i)
    {
        _h.AsyncHandle(CalcOn(i));
    }

    private SomeResponse CalcOn(int i)
    {
        ...;
    }
}

Mo (q) cking Handler dalam ujian Foo, bagaimana saya bisa mengecek apa yang Bar()telah diteruskan _h.AsyncHandle?

Jan
sumber
Apakah maksud Anda "AsyncHandle" (tambahan "n")? Dan bisakah Anda memposting kode untuk Handler, atau menentukan nama tipe yang memenuhi syarat jika itu tipe standar?
TrueWill
Bisakah Anda menunjukkan tes kerangka Anda untuk menunjukkan apa yang Anda pikirkan? Sementara saya menghargai bahwa dari pihak Anda sudah jelas, dari pihak kami, sepertinya seseorang yang tidak meluangkan waktu untuk membuat pertanyaan terjawab tanpa melakukan jawaban spekulatif yang panjang.
Ruben Bartelink
1
Tidak ada Foo atau Bar () atau yang seperti ini. Ini hanya beberapa kode demo untuk menunjukkan situasi saya tanpa gangguan dari spesifikasi aplikasi. Dan saya hanya mendapatkan jawabannya, saya berharap untuk mendapatkannya.
Jan

Jawaban:

283

Anda dapat menggunakan metode Mock.Callback:

var mock = new Mock<Handler>();
SomeResponse result = null;
mock.Setup(h => h.AnsyncHandle(It.IsAny<SomeResponse>()))
    .Callback<SomeResponse>(r => result = r);

// do your test
new Foo(mock.Object).Bar(22);
Assert.NotNull(result);

Jika Anda hanya ingin memeriksa sesuatu yang sederhana pada argumen yang diteruskan, Anda juga dapat melakukannya secara langsung:

mock.Setup(h => h.AnsyncHandle(It.Is<SomeResponse>(response => response != null)));
Gamlor
sumber
36
Catatan tambahan, jika Anda memiliki beberapa argumen untuk fungsi Anda, Anda perlu menentukan semua jenis dalam Callback<>()metode Moq generik . Misalnya, jika metode Anda memiliki definisi Handler.AnsyncHandle(string, SomeResponse), Anda perlu /* ... */.Callback<string, SomeResponse>(r => result = r);. Saya belum menemukan ini secara eksplisit di banyak tempat, jadi saya pikir saya akan menambahkannya di sini.
Frank Bryce
12
Hanya ingin memperbaiki @Jujur jika Anda belum melihat jawaban @JavaJudt. Cara yang tepat untuk mendapatkan dua argumen adalah:/* ... */.Callback<string, SomeResponse>((s1, s2) => { str1 = s1; result = s2});
renatogbp
2
Anda juga dapat mencapai hal yang sama dengan hanya mendeklarasikan jenis argumen dalam ekspresi lambda, seperti:.Callback((string s1, SomeResponse s2) => /* stuff */ )
jb637
1
Bisakah Anda memperbarui tanggapan Anda untuk menyertakan referensi ke Capture.Inpembantu bawaan?
cao
29

Jawaban Gamlor bekerja untuk saya, tetapi saya pikir saya akan memperluas komentar John Carpenter karena saya sedang mencari solusi yang melibatkan lebih dari satu parameter. Saya pikir orang lain yang tersandung ke halaman ini mungkin berada dalam situasi yang sama. Saya menemukan info ini di dokumentasi Moq .

Saya akan menggunakan contoh Gamlor, tapi mari kita anggap metode AsyncHandle membutuhkan dua argumen: a stringdan SomeResponseobjek.

var mock = new Mock<Handler>();
string stringResult = string.Empty;
SomeResponse someResponse = null;
mock.Setup(h => h.AsyncHandle(It.IsAny<string>(), It.IsAny<SomeResponse>()))
    .Callback<string, SomeResponse>((s, r) => 
    {
        stringResult = s;
        someResponse = r;
    });

// do your test
new Foo(mock.Object).Bar(22);
Assert.AreEqual("expected string", stringResult);
Assert.IsNotNull(someResponse);

Pada dasarnya Anda hanya perlu menambahkan yang lain It.IsAny<>()dengan tipe yang sesuai, menambahkan tipe lain ke Callbackmetode, dan mengubah ekspresi lambda yang sesuai.

JavaJudt
sumber
22

Metode Callback pasti akan bekerja, tetapi jika Anda melakukan ini pada metode dengan banyak parameter itu bisa sedikit bertele-tele. Berikut adalah sesuatu yang saya gunakan untuk menghapus beberapa boilerplate.

var mock = new Mock<Handler>();

// do your test   
new Foo(mock.Object).Bar(22);

var arg = new ArgumentCaptor<SomeResponse>();
mock.Verify(h => h.AsyncHandle(arg.Capture()));
Assert.NotNull(arg.Value);

Berikut adalah sumber untuk ArgumentCaptor:

public class ArgumentCaptor<T>
{
    public T Capture()
    {
        return It.Is<T>(t => SaveValue(t));
    }

    private bool SaveValue(T t)
    {
        Value = t;
        return true;
    }

    public T Value { get; private set; }
}
Andrew Radford
sumber
21

Jawaban Gamlor bekerja, tetapi cara lain untuk melakukannya (dan yang saya anggap lebih ekspresif dalam tes) adalah ...

var mock = new Mock<Handler>();
var desiredParam = 47; // this is what you want to be passed to AsyncHandle
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(desiredParam), Times.Once());

Verifikasi sangat kuat, dan layak meluangkan waktu untuk membiasakan diri.

Pete Martin
sumber
15
Pendekatan itu baik-baik saja jika Anda hanya ingin memeriksa apakah suatu metode dipanggil dengan parameter yang dikenal. Dalam kasus di mana parameter belum dibuat pada saat penulisan tes (misalnya unit yang bersangkutan membuat parameter secara internal), maka Callback memungkinkan Anda untuk menangkap dan menginterogasinya, sedangkan pendekatan Anda tidak.
Michael
1
Saya perlu menyimpan nilai yang diteruskan karena saya perlu memverifikasi bahwa semua kumpulan objek di mana berlalu.
MrFox
Juga, kadang-kadang parameternya adalah entitas, dan Anda ingin menegaskan secara terpisah untuk setiap bidang dalam entitas. Cara terbaik untuk melakukannya adalah dengan menangkap param, daripada menggunakan Verifikasi dan korek api.
Kevin Wong
12

Alternatifnya adalah dengan menggunakan Capture.Infitur moq. Ini adalah moqfitur OOTB yang memungkinkan pengambilan argumen dalam koleksi.

//Arrange
var args = new List<SomeResponse>();
mock.Setup(h => h.AnsyncHandle(Capture.In(args)));

//Act
new Foo(mock.Object).Bar(22);

//Assert
//... assert args.Single() or args.First()
Johnny
sumber
1
Jawaban bagus! Saya tidak mengetahui tentang kelas Moq.Capture dan Moq.CaptureMatch sampai melihat jawaban ini. Capture adalah alternatif yang lebih baik untuk CallbackIMO. Karena Anda menggunakan Tangkap langsung dalam daftar parameter, itu jauh lebih rentan terhadap masalah ketika refactoring daftar parameter metode, dan karenanya membuat tes lebih rapuh. Dengan Callback, Anda harus menjaga agar parameter yang lulus dalam Pengaturan tetap sinkron dengan parameter jenis yang digunakan untuk Callback, dan itu pasti telah menyebabkan masalah pada saya di masa lalu.
Justin Holzer
7

Anda bisa menggunakan It.Is<TValue>()pencocokan.

var mock = new Mock<Handler>();
new Foo(mock.Object).Bar(22);
mock.Verify(h => h.AsyncHandle(It.Is<SomeResponse>(r => r != null )));
Ed Yablonsky
sumber
2

Ini juga berfungsi:

Mock<InterfaceThing> mockedObject = new Mock<InterfaceThing>();
var objectParameter = mockedObject.Invocations[1].Arguments[0] as ObjectParameter;
Jeff Smith
sumber
2

Banyak jawaban bagus di sini! Pergilah dengan set fitur yang ada di luar kotak Moq sampai Anda perlu membuat pernyataan tentang beberapa parameter kelas yang diteruskan ke dependensi Anda. Jika Anda berakhir dalam situasi itu, fitur Verifikasi Moq dengan It. Pencocokan tidak melakukan pekerjaan yang baik untuk mengisolasi kegagalan tes, dan cara Pengembalian / Panggilan Balik untuk menangkap argumen menambahkan baris kode yang tidak perlu ke pengujian Anda (dan tes panjang adalah jalan keluar bagi saya).

Berikut adalah intinya: https://gist.github.com/Jacob-McKay/8b8d41ebb9565f5fca23654fd944ac6b dengan ekstensi Moq (4,12) yang saya tulis yang memberikan cara yang lebih deklaratif untuk membuat pernyataan tentang argumen yang dilontarkan ke ejek, tanpa kekurangan yang disebutkan di atas. Berikut ini bagian Verify sekarang:

        mockDependency
            .CheckMethodWasCalledOnce(nameof(IExampleDependency.PersistThings))
            .WithArg<InThing2>(inThing2 =>
            {
                Assert.Equal("Input Data with Important additional data", inThing2.Prop1);
                Assert.Equal("I need a trim", inThing2.Prop2);
            })
            .AndArg<InThing3>(inThing3 =>
            {
                Assert.Equal("Important Default Value", inThing3.Prop1);
                Assert.Equal("I NEED TO BE UPPER CASED", inThing3.Prop2);
            });

Saya akan terpacu jika Moq menyediakan fitur yang melakukan hal yang sama sementara menjadi deklaratif dan menyediakan isolasi kegagalan ini. Semoga saja!

Jacob McKay
sumber
1
Saya suka ini. Verifikasi Moq bersaing dengan Assert of xUnit untuk otoritas melakukan penegasan. Itu tidak terasa benar pada bagian Moq dari pengaturan. Pengaturan fitur It.Is juga harus mendukung non ekspresi.
Thomas
"Moq bersaing dengan Assert of xUnit untuk otoritas melakukan penegasan" - Nah kata @Thomas. Saya akan menambahkan bahwa itu akan kehilangan persaingan. Moq pandai memberi tahu Anda apakah ada panggilan yang cocok, tetapi pernyataan spesifik memberi Anda informasi yang jauh lebih baik. Kelemahan utama pendekatan saya adalah kehilangan jenis keamanan dan pemeriksaan urutan parameter. Saya telah mencari perbaikan atas ini untuk sementara waktu, semoga ada C # ninja di luar sana yang bisa meretas contoh bersama! Kalau tidak, jika saya menemukan cara saya akan memperbarui ini.
Jacob McKay