Nilai pengembalian yang berbeda untuk pertama dan kedua dengan Moq

262

Saya memiliki tes seperti ini:

    [TestCase("~/page/myaction")]
    public void Page_With_Custom_Action(string path) {
        // Arrange
        var pathData = new Mock<IPathData>();
        var pageModel = new Mock<IPageModel>();
        var repository = new Mock<IPageRepository>();
        var mapper = new Mock<IControllerMapper>();
        var container = new Mock<IContainer>();

        container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);

        repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageModel.Object);

        pathData.Setup(x => x.Action).Returns("myaction");
        pathData.Setup(x => x.Controller).Returns("page");

        var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

        // Act
        var data = resolver.ResolvePath(path);

        // Assert
        Assert.NotNull(data);
        Assert.AreEqual("myaction", data.Action);
        Assert.AreEqual("page", data.Controller);
    }

GetPageByUrlberjalan dua kali di saya DashboardPathResolver, bagaimana saya bisa memberitahu Moq untuk kembali nullpertama kali dan pageModel.Objectyang kedua?

marcus
sumber

Jawaban:

454

Dengan versi terbaru Moq (4.2.1312.1622), Anda dapat mengatur urutan acara menggunakan SetupSequence . Ini sebuah contoh:

_mockClient.SetupSequence(m => m.Connect(It.IsAny<String>(), It.IsAny<int>(), It.IsAny<int>()))
        .Throws(new SocketException())
        .Throws(new SocketException())
        .Returns(true)
        .Throws(new SocketException())
        .Returns(true);

Panggilan panggilan hanya akan berhasil pada upaya ketiga dan kelima jika tidak pengecualian akan dilemparkan.

Jadi untuk contoh Anda itu hanya akan menjadi seperti:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);
stackunderflow
sumber
2
Jawaban yang bagus, satu-satunya batasan adalah "SetupSequence" tidak bekerja dengan anggota yang dilindungi.
Chasefornone
7
Sayangnya, SetupSequence()tidak berhasil Callback(). Jika itu benar, seseorang dapat memverifikasi panggilan ke metode yang diolok-olok dengan cara "mesin negara".
urig
@stackunderflow SetupSequencehanya berfungsi untuk dua panggilan tapi apa yang bisa saya lakukan jika perlu lebih dari dua panggilan?
TanvirArjel
@TanvirArjel, tidak yakin apa maksud Anda ... SetupSequencedapat digunakan untuk jumlah panggilan yang sewenang-wenang. Contoh pertama yang saya berikan mengembalikan urutan 5 panggilan.
stackunderflow
@stackunderflow Maaf! Ini adalah kesalahpahaman saya! Iya! Anda benar itu berfungsi seperti yang diharapkan!
TanvirArjel
115

Jawaban yang ada bagus, tapi saya pikir saya akan menggunakan alternatif saya yang hanya menggunakan System.Collections.Generic.Queuedan tidak memerlukan pengetahuan khusus tentang kerangka mengejek - karena saya tidak punya ketika saya menulisnya! :)

var pageModel = new Mock<IPageModel>();
IPageModel pageModelNull = null;
var pageModels = new Queue<IPageModel>();
pageModels.Enqueue(pageModelNull);
pageModels.Enqueue(pageModel.Object);

Kemudian...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);
mo.
sumber
Terima kasih. Saya baru saja memperbaiki kesalahan ketik di mana saya melakukan enqueue mock pageModel bukan pageModel.Object, jadi sekarang bahkan harus membangun juga! :)
mo.
3
Jawabannya benar, tetapi perhatikan bahwa ini tidak akan berhasil jika Anda ingin melempar Exceptionkarena Anda tidak Enqueuebisa. Tetapi SetupSequenceakan berfungsi (lihat jawaban dari @stackunderflow misalnya).
Halvard
4
Anda harus menggunakan metode yang didelegasikan untuk Dequeue. Cara sampel ditulis itu akan selalu mengembalikan item pertama dalam antrian berulang kali, karena dequeue dievaluasi pada saat pengaturan.
Jason Coyne
7
Itu adalah delegasi. Jika kode berisi Dequeue()bukan hanya Dequeue, Anda akan benar.
mo.
31

Menambahkan panggilan balik tidak berfungsi untuk saya, saya menggunakan pendekatan ini sebagai gantinya http://haacked.com/archive/2009/09/29/moq- berikutnyaences.aspx dan saya berakhir dengan tes seperti ini:

    [TestCase("~/page/myaction")]
    [TestCase("~/page/myaction/")]
    public void Page_With_Custom_Action(string virtualUrl) {

        // Arrange
        var pathData = new Mock<IPathData>();
        var pageModel = new Mock<IPageModel>();
        var repository = new Mock<IPageRepository>();
        var mapper = new Mock<IControllerMapper>();
        var container = new Mock<IContainer>();

        container.Setup(x => x.GetInstance<IPageRepository>()).Returns(repository.Object);
        repository.Setup(x => x.GetPageByUrl<IPageModel>(virtualUrl)).ReturnsInOrder(null, pageModel.Object);

        pathData.Setup(x => x.Action).Returns("myaction");
        pathData.Setup(x => x.Controller).Returns("page");

        var resolver = new DashboardPathResolver(pathData.Object, repository.Object, mapper.Object, container.Object);

        // Act
        var data = resolver.ResolvePath(virtualUrl);

        // Assert
        Assert.NotNull(data);
        Assert.AreEqual("myaction", data.Action);
        Assert.AreEqual("page", data.Controller);
    }
marcus
sumber
29

Anda dapat menggunakan panggilan balik saat mengatur objek tiruan Anda. Lihatlah contoh dari Moq Wiki ( http://code.google.com/p/moq/wiki/QuickStart ).

// returning different values on each invocation
var mock = new Mock<IFoo>();
var calls = 0;
mock.Setup(foo => foo.GetCountThing())
    .Returns(() => calls)
    .Callback(() => calls++);
// returns 0 on first invocation, 1 on the next, and so on
Console.WriteLine(mock.Object.GetCountThing());

Pengaturan Anda mungkin terlihat seperti ini:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });
Dan
sumber
1
Saya mendapatkan null saat saya melakukan ini: var pageModel = new Mock <IPageModel> (); Model IPageModel = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (path)). Mengembalikan (() => model) .Callback (() => {model = pageModel.Object;});
marcus
Apakah GetPageByUrl dipanggil dua kali dalam metode resolver.ResolvePath?
Dan
ResolvePath berisi kode di bawah ini tetapi masih nol dua kali var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
marcus
2
Dikonfirmasi bahwa pendekatan panggilan balik tidak berfungsi (bahkan dicoba di versi Moq sebelumnya). Pendekatan lain yang mungkin - tergantung pada tes Anda - adalah hanya melakukan Setup()panggilan lagi, dan Return()nilai yang berbeda.
Kent Boogaart
4

Mencapai di sini untuk jenis masalah yang sama dengan persyaratan yang sedikit berbeda.
Saya perlu mendapatkan nilai kembali yang berbeda dari mock berdasarkan nilai input yang berbeda dan menemukan solusi yang IMO lebih mudah dibaca karena menggunakan sintaks deklaratif Moq (linq to Mocks).

public interface IDataAccess
{
   DbValue GetFromDb(int accountId);  
}

var dataAccessMock = Mock.Of<IDataAccess>
(da => da.GetFromDb(It.Is<int>(acctId => acctId == 0)) == new Account { AccountStatus = AccountStatus.None }
&& da.GetFromDb(It.Is<int>(acctId => acctId == 1)) == new DbValue { AccountStatus = AccountStatus.InActive }
&& da.GetFromDb(It.Is<int>(acctId => acctId == 2)) == new DbValue { AccountStatus = AccountStatus.Deleted });

var result1 = dataAccessMock.GetFromDb(0); // returns DbValue of "None" AccountStatus
var result2 = dataAccessMock.GetFromDb(1); // returns DbValue of "InActive"   AccountStatus
var result3 = dataAccessMock.GetFromDb(2); // returns DbValue of "Deleted" AccountStatus
Saravanan
sumber
Bagi saya (Moq 4.13.0 dari 2019 di sini), itu bekerja bahkan dengan yang lebih pendek da.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ..., tanpa It.Is-lambda diperlukan sama sekali.
ojdo
3

The jawaban yang diterima , serta jawaban SetupSequence , menangani kembali konstanta.

Returns()memiliki beberapa kelebihan yang berguna di mana Anda dapat mengembalikan nilai berdasarkan parameter yang dikirim ke metode yang diejek. Berdasarkan solusi yang diberikan dalam jawaban yang diterima, berikut adalah metode ekstensi lain untuk kelebihan itu.

public static class MoqExtensions
{
    public static IReturnsResult<TMock> ReturnsInOrder<TMock, TResult, T1>(this ISetup<TMock, TResult> setup, params Func<T1, TResult>[] valueFunctions)
        where TMock : class
    {
        var queue = new Queue<Func<T1, TResult>>(valueFunctions);
        return setup.Returns<T1>(arg => queue.Dequeue()(arg));
    }
}

Sayangnya, menggunakan metode ini mengharuskan Anda untuk menentukan beberapa parameter templat, tetapi hasilnya masih cukup mudah dibaca.

repository
    .Setup(x => x.GetPageByUrl<IPageModel>(path))
    .ReturnsInOrder(new Func<string, IPageModel>[]
        {
            p => null, // Here, the return value can depend on the path parameter
            p => pageModel.Object,
        });

Buat overloads untuk metode ekstensi dengan beberapa parameter ( T2, T3, dll) jika diperlukan.

Torbjörn Kalin
sumber