Mocking method with Action parameter

2019-02-16 20:39发布

[unit testing newbie] [c#]

Consider the following scenario:

I'm using Silverlight and calling a WCF service. Silverlight can only call WCF services asynchronously. I build a wrapper around the WCF service so that I can work with Action parameters. (makes the client code a lot cleaner).

So I have an async service that retrieves meeting rooms.

public interface IMeetingRoomService
{
    void GetRooms(Action<List<MeetingRoom>> result);
}

Turning GetRooms into List<MeetingRoom> GetRooms() is not an option.

I want to use this service in a ViewModel to set a public property called Rooms.

public class SomeViewModel
{
    private readonly IMeetingRoomService _meetingRoomService;

    public List<MeetingRoom> Rooms { get; set; }

    public SomeViewModel(IMeetingRoomService meetingRoomService)
    {
        this._meetingRoomService = meetingRoomService;
    }

    public void GetRooms()
    {
        // Code that calls the service and sets this.Rooms
        _meetingRoomService.GetRooms(result => Rooms = result);
    }
}

I want to unit test the implementation of SomeViewModel.GetRooms(). (For this question I quickly wrote the implementation but I'm actually trying to use TDD.)

How do I finish this test? I'm using NUnit and Moq.

[Test]
public void GetRooms_ShouldSetRooms()
{
    var theRooms = new List<MeetingRoom>
                       {
                           new MeetingRoom(1, "some room"),
                           new MeetingRoom(2, "some other room"),
                       };

    var meetingRoomService = new Mock<IMeetingRoomService>();

    //How do I setup meetingRoomService so that it gives theRooms in the Action??


    var viewModel = new SomeViewModel(meetingRoomService.Object);

    viewModel.GetRooms();

    Assert.AreEqual(theRooms, viewModel .Rooms);
}

EDIT:

Solution

Read Stephane's answer first.

This is the Test code I ended up writing thanks to stephane's answer:

[Test]
public void GetRooms_ShouldSetRooms()
{
    var meetingRoomService = new Mock<IMeetingRoomService>();
    var shell = new ShellViewModel(meetingRoomService.Object);
    var theRooms = new List<MeetingRoom>
                       {
                           new MeetingRoom(1, "some room"),
                           new MeetingRoom(2, "some other room"),
                       };

    meetingRoomService
        .Setup(service => service.GetRooms(It.IsAny<Action<List<MeetingRoom>>>()))
        .Callback((Action<List<MeetingRoom>> action) => action(theRooms));

    shell.GetRooms();

    Assert.AreEqual(theRooms, shell.Rooms);
}

2条回答
干净又极端
2楼-- · 2019-02-16 21:25

You could use an AutoResetEvent to handle async calls.

Just initialize it as unset and configure your mock service to set it in the callback. (IE: var mockService = new Mock(); mockService.SetUp(x => x.MyMethod()).Returns(someStuff).Callback(() => handle.Set()); )

After that I use hadle.WaitOne(1000) to check if it was called. (IMHO 1000 miliseconds are more than enough to run the async code).

Sorry: This was supposed to go as a reply to the post above... I can't for the life of me figure out how to reply :)

查看更多
祖国的老花朵
3楼-- · 2019-02-16 21:32

Here is some pseudo code, I haven't run it. But I think that's what you want.

SetupCallback is what you are interested in.

For all the calls to _meetingRoomServiceFake.GetRooms, simply set the _getRoomsCallback to the parameter passed in.

You now have a reference to the callback that you are passing in your viewmodel, and you can call it with whatever list of MeetingRooms you want to test it. So you can test your asynchronous code almost the same way as synchronous code. it's just a bit more ceremony to setup the fake.

Action<List<MeetingRoom>> _getRoomsCallback = null;
IMeetingRoomService _meetingRoomServiceFake;


private void SetupCallback()
{
     Mock.Get(_meetingRoomServiceFake)
         .Setup(f => f.GetRooms(It.IsAny<Action<List<MeetingRoom>>>()))
         .Callback((Action<List<MeetingRoom>> cb) => _getRoomsCallback= cb);
}

[Setup]
public void Setup()
{
     _meetingRoomServiceFake = Mock.Of<IMeetingRoomService>();
     SetupCallback();
}

[Test]
public void Test()
{

      var viewModel = new SomeViewModel(_meetingRoomServiceFake)

      //in there the mock gets called and sets the _getRoomsCallback field.
      viewModel.GetRooms();
      var theRooms = new List<MeetingRoom>
                   {
                       new MeetingRoom(1, "some room"),
                       new MeetingRoom(2, "some other room"),
                   };

     //this will call whatever was passed as callback in your viewModel.
     _getRoomsCallback(theRooms);
}
查看更多
登录 后发表回答