การคืนค่าที่แตกต่างกันเป็นครั้งแรกและครั้งที่สองด้วย Moq


262

ฉันมีแบบทดสอบนี้:

    [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);
    }

GetPageByUrlทำงานสองครั้งในตัวฉันฉันDashboardPathResolverจะบอกให้ Moq กลับมาnullเป็นครั้งแรกและpageModel.Objectครั้งที่สองได้อย่างไร

คำตอบ:


452

กับรุ่นล่าสุดของ Moq (4.2.1312.1622) คุณสามารถตั้งค่าลำดับของเหตุการณ์โดยใช้SetupSequence นี่คือตัวอย่าง:

_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);

การเชื่อมต่อการโทรจะทำได้สำเร็จในการพยายามครั้งที่สามและครั้งที่ห้ามิฉะนั้นจะมีข้อผิดพลาดเกิดขึ้น

ดังนั้นสำหรับตัวอย่างของคุณมันก็จะเป็นเช่น:

repository.SetupSequence(x => x.GetPageByUrl<IPageModel>(virtualUrl))
.Returns(null)
.Returns(pageModel.Object);

2
คำตอบที่ดีข้อ จำกัด เพียงอย่างเดียวคือ "SetupSequence" ไม่ทำงานกับสมาชิกที่ได้รับความคุ้มครอง
Chasefornone

7
อนิจจาไม่ได้ทำงานกับSetupSequence() Callback()หากทำได้เพียงอย่างเดียวเราสามารถตรวจสอบการโทรไปยังวิธีการเยาะเย้ยในรูปแบบ "state machine"
urig

@stackunderflow SetupSequenceใช้งานได้สำหรับการโทรสองครั้ง แต่ฉันจะต้องทำอย่างไรหากต้องการการโทรมากกว่าสองสาย
TanvirArjel

@TanvirArjel ไม่แน่ใจว่าคุณหมายถึงอะไร ... SetupSequenceสามารถใช้สำหรับการโทรจำนวนเท่าใดก็ได้ ตัวอย่างแรกที่ฉันส่งคืนจะมีการโทร 5 ครั้ง
stackunderflow

@stackunderflow ขออภัย! นี่เป็นความเข้าใจผิดของฉัน! ใช่ คุณแก้ไขให้ถูกต้องตามที่คาดไว้!
TanvirArjel

115

คำตอบที่มีอยู่นั้นยอดเยี่ยม แต่ฉันคิดว่าฉันจะเลือกใช้สิ่งที่ฉันเพิ่งใช้System.Collections.Generic.Queueและไม่ต้องการความรู้พิเศษเกี่ยวกับกรอบการเยาะเย้ย - เนื่องจากฉันไม่มีอะไรเลยเมื่อฉันเขียนมัน! :)

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

จากนั้น ...

repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(pageModels.Dequeue);

ขอบคุณ ฉันเพิ่งแก้ไขพิมพ์ผิดที่ฉัน enqueueing pageModel จำลองแทน pageModel.Object ดังนั้นตอนนี้มันควรจะสร้างเกินไป! :)
mo

3
คำตอบที่ถูกต้อง แต่ทราบว่านี้จะไม่ทำงานถ้าคุณต้องการที่จะโยนExceptionที่คุณไม่สามารถEnqueueมัน แต่SetupSequenceจะใช้งานได้ (ดูคำตอบจาก @stackunderflow เป็นต้น)
Halvard

4
คุณต้องใช้วิธีการมอบหมายสำหรับ Dequeue วิธีที่ตัวอย่างถูกเขียนมันจะส่งคืนรายการแรกในคิวซ้ำ ๆ เสมอเนื่องจากการถอนออกจากกันจะถูกประเมินในเวลาที่ตั้งค่า
Jason Coyne

7
นั่นคือตัวแทน หากรหัสมีอยู่Dequeue()แทนที่จะเป็นเพียงDequeueคุณจะถูกต้อง
โม

31

การเพิ่มการโทรกลับไม่ทำงานสำหรับฉันฉันใช้วิธีนี้แทนhttp://haacked.com/archive/2009/09/29/moq-sequences.aspxและฉันสิ้นสุดการทดสอบเช่นนี้:

    [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);
    }

29

คุณสามารถใช้การโทรกลับเมื่อตั้งค่าวัตถุจำลองของคุณ ดูตัวอย่างจาก 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());

การตั้งค่าของคุณอาจมีลักษณะเช่นนี้:

var pageObject = pageModel.Object;
repository.Setup(x => x.GetPageByUrl<IPageModel>(path)).Returns(() => pageObject).Callback(() =>
            {
                // assign new value for second call
                pageObject = new PageModel();
            });

1
ฉันได้รับโมฆะทั้งเวลาที่ฉันทำเช่นนี้: var pageModel = new Mock <IPageModel> (); IPageModel model = null; repository.Setup (x => x.GetPageByUrl <IPageModel> (เส้นทาง)) ผลตอบแทน (() => รุ่น) .Callback (() => {model = pageModel.Object;});
marcus

GetPageByUrl เรียกว่าสองครั้งภายในวิธี resolver.ResolvePath หรือไม่
ด่าน

ResolvePath มีรหัสด้านล่าง แต่ก็ยังว่างทั้งสองครั้ง var foo = _repository.GetPageByUrl <IPageModel> (virtualUrl); var foo2 = _repository.GetPageByUrl <IPageModel> (virtualUrl);
มาร์คัส

2
ยืนยันว่าวิธีการโทรกลับไม่ทำงาน (แม้จะทดลองใช้ในเวอร์ชั่น Moq ก่อนหน้านี้) อีกวิธีที่เป็นไปได้ - ขึ้นอยู่กับการทดสอบของคุณ - คือทำการSetup()โทรอีกครั้งและReturn()ค่าอื่น
Kent Boogaart


4

มาถึงที่นี่สำหรับปัญหาประเภทเดียวกันที่มีข้อกำหนดแตกต่างกันเล็กน้อย
ฉันต้องการได้รับค่าตอบแทนที่แตกต่างจากการจำลองตามค่าอินพุตที่แตกต่างกันและวิธีแก้ปัญหาที่พบซึ่ง IMO อ่านได้มากขึ้นเนื่องจากใช้ไวยากรณ์ที่ประกาศของ Moq (linq ถึง 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

สำหรับฉัน (ขั้นต่ำ 4.13.0 จาก 2019 ที่นี่) มันทำงานได้แม้จะมีขนาดที่สั้นda.GetFromDb(0) == new Account { ..None.. && da.GetFromDb(1) == new Account { InActive } && ...และไม่It.Isจำเป็นต้องใช้แลมบ์ดาเลย
ojdo

3

คำตอบที่ได้รับการยอมรับเช่นเดียวกับคำตอบ SetupSequenceจับกลับมาคงที่

Returns()มี overloads ที่มีประโยชน์บางอย่างที่คุณสามารถส่งคืนค่าตามพารามิเตอร์ที่ส่งไปยังวิธีการจำลอง ขึ้นอยู่กับวิธีแก้ปัญหาที่ระบุในคำตอบที่ยอมรับนี่เป็นอีกวิธีการขยายสำหรับการโอเวอร์โหลดเหล่านั้น

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));
    }
}

น่าเสียดายที่การใช้วิธีนี้คุณจะต้องระบุพารามิเตอร์เทมเพลต แต่ผลลัพธ์ยังคงสามารถอ่านได้

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,
        });

สร้าง overloads สำหรับวิธีขยายที่มีหลายพารามิเตอร์ ( T2, T3ฯลฯ ) หากมีความจำเป็น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.