การตรวจสอบพารามิเตอร์เฉพาะด้วย Moq


170
public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    var queueableMessage = CreateSingleQueueableMessage();
    var message = queueableMessage[0];
    var xml = QueueableMessageAsXml(queueableMessage);
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(xml)).Verifiable();
    //messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();

    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(essageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    //messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(xml), Times.Once());
    messageServiceClientMock.Verify();
}

ฉันเริ่มใช้ Moq และดิ้นรนเล็กน้อย ฉันพยายามยืนยันว่า messageServiceClient ได้รับพารามิเตอร์ที่ถูกต้องซึ่งเป็น XmlElement แต่ฉันไม่สามารถหาวิธีที่จะทำให้มันใช้งานได้ ใช้งานได้เฉพาะเมื่อฉันไม่ได้ตรวจสอบค่าเฉพาะ

ความคิดใด ๆ

คำตอบบางส่วน: ฉันพบวิธีที่จะทดสอบว่า xml ที่ส่งไปยังพร็อกซีถูกต้อง แต่ฉันยังไม่คิดว่ามันเป็นวิธีที่ถูกต้องที่จะทำ

public void SubmitMessagesToQueue_OneMessage_SubmitSuccessfully()
{
    var messageServiceClientMock = new Mock<IMessageServiceClient>();
    messageServiceClientMock.Setup(proxy => proxy.SubmitMessage(It.IsAny<XmlElement>())).Verifiable();
    var serviceProxyFactoryStub = new Mock<IMessageServiceClientFactory>();
    serviceProxyFactoryStub.Setup(proxyFactory => proxyFactory.CreateProxy()).Returns(messageServiceClientMock.Object);
    var loggerStub = new Mock<ILogger>();

    var client = new MessageClient(serviceProxyFactoryStub.Object, loggerStub.Object);
    var message = CreateMessage();
    client.SubmitMessagesToQueue(new List<IMessageRequestDTO> {message});

    messageServiceClientMock.Verify(proxy => proxy.SubmitMessage(It.Is<XmlElement>(xmlElement => XMLDeserializer<QueueableMessage>.Deserialize(xmlElement).Messages.Contains(message))), Times.Once());
}

ยังไงฉันจะแยกการแสดงออกจากการโทรยืนยันได้อย่างไร

คำตอบ:


251

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

อีกตัวเลือกหนึ่งคือการใช้การโทรกลับในการเรียกใช้การตั้งค่าเพื่อเก็บค่าที่ส่งผ่านไปยังวิธีการเยาะเย้ยแล้วเขียนAssertวิธีมาตรฐานเพื่อตรวจสอบความถูกต้อง ตัวอย่างเช่น:

// Arrange
MyObject saveObject;
mock.Setup(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()))
        .Callback<int, MyObject>((i, obj) => saveObject = obj)
        .Returns("xyzzy");

// Act
// ...

// Assert
// Verify Method was called once only
mock.Verify(c => c.Method(It.IsAny<int>(), It.IsAny<MyObject>()), Times.Once());
// Assert about saveObject
Assert.That(saveObject.TheProperty, Is.EqualTo(2));

6
ข้อดีอย่างหนึ่งของวิธีนี้คือมันจะให้การทดสอบล้มเหลวโดยเฉพาะสำหรับวิธีการที่วัตถุไม่ถูกต้อง
Rob Church

1
ฉันคิดว่าฉันเป็นคนเดียวที่ทำสิ่งนี้ดีใจที่ได้เห็นว่ามันเป็นวิธีการที่สมเหตุสมผล!
Appleby จะ

3
ผมคิดว่าการใช้ It.Is <MyObject> (ตรวจสอบ) ตามเมโยจะดีกว่าในขณะที่มันหลีกเลี่ยงวิธีการที่น่าอึดอัดใจเล็กน้อยของการประหยัดค่าพารามิเตอร์ที่เป็นส่วนหนึ่งของแลมบ์ดา
stevec

เธรดนี้ปลอดภัยหรือไม่ตัวอย่างเช่นเมื่อรันการทดสอบแบบขนาน?
Anton Tolken

@AntonTolken ฉันยังไม่ได้ลอง แต่ในตัวอย่างของฉันวัตถุที่ได้รับการปรับปรุงเป็นตัวแปรเฉพาะที่ (saveObject) ดังนั้นจึงควรมีความปลอดภัยสำหรับเธรด
คนรวย Tebb

113

ฉันได้รับการยืนยันการโทรในลักษณะเดียวกัน - ฉันเชื่อว่ามันเป็นวิธีที่เหมาะสมที่จะทำ

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => mo.Id == 5 && mo.description == "test")
  ), Times.Once());

หากการแสดงออกแลมบ์ดาของคุณไม่สะดวกคุณสามารถสร้างฟังก์ชั่นที่ใช้MyObjectเป็นอินพุตและเอาต์พุตtrue/ false...

mockSomething.Verify(ms => ms.Method(
    It.IsAny<int>(), 
    It.Is<MyObject>(mo => MyObjectFunc(mo))
  ), Times.Once());

private bool MyObjectFunc(MyObject myObject)
{
  return myObject.Id == 5 && myObject.description == "test";
}

นอกจากนี้ให้ระวังข้อผิดพลาดด้วย Mock ซึ่งข้อความแสดงข้อผิดพลาดระบุว่าวิธีการนั้นถูกเรียกหลายครั้งเมื่อไม่ได้เรียกเลย พวกเขาอาจแก้ไขได้ในตอนนี้ แต่ถ้าคุณเห็นข้อความนั้นคุณอาจลองตรวจสอบว่าวิธีการนั้นถูกเรียกใช้จริง

แก้ไข: นี่คือตัวอย่างของการโทรตรวจสอบหลายครั้งสำหรับสถานการณ์เหล่านั้นที่คุณต้องการตรวจสอบว่าคุณเรียกใช้ฟังก์ชั่นสำหรับแต่ละวัตถุในรายการ (ตัวอย่าง)

foreach (var item in myList)
  mockRepository.Verify(mr => mr.Update(
    It.Is<MyObject>(i => i.Id == item.Id && i.LastUpdated == item.LastUpdated),
    Times.Once());

วิธีการเดียวกันสำหรับการติดตั้ง ...

foreach (var item in myList) {
  var stuff = ... // some result specific to the item
  this.mockRepository
    .Setup(mr => mr.GetStuff(item.itemId))
    .Returns(stuff);
}

ดังนั้นทุกครั้งที่ GetStuff ถูกเรียกใช้สำหรับ itemId มันจะส่งคืนสิ่งที่เฉพาะเจาะจงสำหรับรายการนั้น หรือคุณสามารถใช้ฟังก์ชั่นที่รับ itemId เป็นอินพุตและส่งคืนข้อมูลได้

this.mockRepository
    .Setup(mr => mr.GetStuff(It.IsAny<int>()))
    .Returns((int id) => SomeFunctionThatReturnsStuff(id));

อีกวิธีหนึ่งที่ฉันเห็นในบล็อกบางครั้ง (Phil Haack อาจจะ?) มีการตั้งค่ากลับมาจากวัตถุ dequeue บางชนิด - ในแต่ละครั้งที่ฟังก์ชั่นถูกเรียกมันจะดึงรายการจากคิว


1
ขอบคุณมันทำให้รู้สึกถึงฉัน สิ่งที่ฉันยังไม่เข้าใจคือเมื่อต้องระบุรายละเอียดในการตั้งค่าหรือตรวจสอบ มันค่อนข้างสับสน ในตอนนี้ฉันแค่อนุญาตทุกอย่างในการตั้งค่าและระบุค่าในยืนยัน
Luis Mirabal

คุณคิดว่าฉันสามารถตรวจสอบข้อความเมื่อมีการโทรหลายสายได้อย่างไร? ไคลเอนต์ใช้ข้อความและสามารถสร้างหลายคิวข้อความซึ่งจะสิ้นสุดในการโทรหลายครั้งและในแต่ละสายเหล่านั้นฉันต้องตรวจสอบข้อความที่แตกต่างกัน ฉันยังคงดิ้นรนกับการทดสอบหน่วยโดยทั่วไปฉันยังไม่คุ้นเคยกับมัน
Luis Mirabal

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

ฉันเพิ่มตัวอย่างสำหรับการโทรหลายสาย - ดูคำตอบด้านบน
Mayo

1
"นอกจากนี้ให้ระวังข้อผิดพลาดกับ Mock ซึ่งข้อความแสดงข้อผิดพลาดระบุว่าวิธีการนั้นถูกเรียกหลายครั้งเมื่อไม่ได้เรียกใช้เลยพวกเขาอาจแก้ไขได้ในตอนนี้ - แต่ถ้าคุณเห็นข้อความนั้นคุณอาจพิจารณายืนยันว่า จริง ๆ แล้ววิธีการนั้นถูกเรียกว่า " - ข้อผิดพลาดเช่นนี้จะทำให้ห้องสมุด IMHO ลอกเลียนแบบไม่สมบูรณ์ วิธีแดกดันพวกเขาไม่ได้มีรหัสทดสอบที่เหมาะสมสำหรับมัน :-)
Gianluca Ghettini

20

วิธีที่ง่ายกว่าคือ:

ObjectA.Verify(
    a => a.Execute(
        It.Is<Params>(p => p.Id == 7)
    )
);

ฉันไม่สามารถทำงานนี้ได้ฉันพยายามยืนยันว่าวิธีการของฉันถูกเรียกใช้โดยมี Code.WRCC เป็นพารามิเตอร์ แต่การทดสอบของฉันเสมอผ่านแม้ว่าพารามิเตอร์ที่ผ่านมาเป็น WRDD .. m.Setup(x => x.CreateSR(Code.WRDD)).ReturnsAsync(0); await ClassUnderTest.CompleteCC(accountKey, It.IsAny<int>(), mockRequest); m.Verify(x => x.CreateSR(It.Is<Code>(p => p == Code.WRCC)), Times.Once());
เกร็กควินน์

1

ฉันเชื่อว่าปัญหาในความจริงที่ Moq จะตรวจสอบความเท่าเทียมกัน และเนื่องจาก XmlElement ไม่ได้แทนที่ Equals การดำเนินการจะตรวจสอบความเท่าเทียมกันของการอ้างอิง

คุณไม่สามารถใช้วัตถุที่กำหนดเองเพื่อให้คุณสามารถแทนที่เท่ากับ?


ใช่ฉันลงเอยด้วยการทำเช่นนั้น ฉันรู้ว่าปัญหากำลังตรวจสอบ Xml ในส่วนที่สองของคำถามฉันได้เพิ่มคำตอบที่เป็นไปได้ deserialising XML ไปยังวัตถุ
Luis Mirabal

1

มีหนึ่งในสิ่งเหล่านี้เช่นกัน แต่พารามิเตอร์ของการดำเนินการเป็นอินเทอร์เฟซที่ไม่มีคุณสมบัติสาธารณะ ลงเอยด้วยการใช้ It.Is () ด้วยเมธอด seperate และภายในเมธอดนี้ต้องทำการเยาะเย้ยของอินเตอร์เฟส

public interface IQuery
{
    IQuery SetSomeFields(string info);
}

void DoSomeQuerying(Action<IQuery> queryThing);

mockedObject.Setup(m => m.DoSomeQuerying(It.Is<Action<IQuery>>(q => MyCheckingMethod(q)));

private bool MyCheckingMethod(Action<IQuery> queryAction)
{
    var mockQuery = new Mock<IQuery>();
    mockQuery.Setup(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition())
    queryAction.Invoke(mockQuery.Object);
    mockQuery.Verify(m => m.SetSomeFields(It.Is<string>(s => s.MeetsSomeCondition(), Times.Once)
    return true
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.