Moq: Advanced Mock Setup

I am relatively new to Moq and have this complex case to mock and am kind of stuck. I was hoping an experienced Moq user could advice me on this:

Inside my ViewModel the ctor is calling this load method:

public void LoadCategories()
        {
            Categories = null;
            BookDataService.GetCategories(GetCategoriesCallback);
        }

I would like to mock the Service obviously. But since the service's method are void and the return is always through a callback, it gets too complicated for me.

private void GetCategoriesCallback(ObservableCollection<Category> categories)
        {
            if (categories != null)
            {
                this.Categories = categories;
                if (Categories.Count > 0)
                {
                    SelectedCategory = Categories[0];
                }
                LoadBooksByCategory();
            }
        }

As this was not bad enough, as you can see there is another LoadMethod within called LoadBooksByCategory()

public void LoadBooksByCategory()
        {
            Books = null;
            if (SelectedCategory != null)
                BookDataService.GetBooksByCategory(GetBooksCallback, SelectedCategory.CategoryID, _pageSize);
        }

private void GetBooksCallback(ObservableCollection<Book> books)
        {
            if (books != null)
            {
                if (Books == null)
                {
                    Books = books;
                }
                else
                {
                    foreach (var book in books)
                    {
                        Books.Add(book);
                    }
                }

                if (Books.Count > 0)
                {
                    SelectedBook = Books[0];
                }
            }
        }

So now my Mock setup:

bool submitted = false;
                Category selectedCategory = new Category{CategoryID = 1};
                ObservableCollection<Book> books;
                var mockDomainClient = new Mock<TestDomainClient>();
                var context = new BookClubContext(mockDomainClient.Object);
                var book = new Book
                {
                 ...
                };

                var entityChangeSet = context.EntityContainer.GetChanges();
                var mockService = new Mock<BookDataService>(context);

                mockService.Setup(s => s.GetCategories(It.IsAny<Action<ObservableCollection<Category>>>()))
                    .Callback<Action<ObservableCollection<Category>>>(action => action(new ObservableCollection<Category>{new Category{CategoryID = 1}}));

                mockService.Setup(s => s.GetBooksByCategory(It.IsAny<Action<ObservableCollection<Book>>>(), selectedCategory.CategoryID, 10))
                    .Callback<Action<ObservableCollection<Book>>>(x => x(new ObservableCollection<Book>()));


                //Act
                var vm = new BookViewModel(mockService.Object);

                vm.AddNewBook(book);
                vm.OnSaveBooks();

                //Assert
                EnqueueConditional(() => vm.Books.Count > 0);
                EnqueueCallback(() => Assert.IsTrue(submitted));

As you can see, I have created two Setups for each Service Call, however due their callback and sequential dependency, its highly confusing.

for example, the second service call GetBooksByCategory() would never be called if Selectedcategory property within the viewmodel remains null. But the only thing I can mock here is really just the service injected into the viewmodel. So how am I going to affect that inside the viewmodel through my callback? :) Does it make sense?

At the very end, I am expecting that the ObservableCollection Books is instantiated and maybe populated with some test data (which I am not doing here, I am happy if its at least instantiated, so that I can test adding a new book to an empty collection)

Thats the idea. Once I can understand this, I think I understand Moq properly. :)


From a Moq perspective, everything you are doing is technically correct. You are using Moq's Callback mechanism, which is typically used for inspecting inbound parameters but in this case you're invoking custom logic to simulate what the service does. If you configure the Mocks to return the correct values, you should be able to exercise the logic in your presentation model. You'll need several tests with different return values to properly exercise all paths of execution. To your point, it's going to get confusing.

You can probably clean things up a bit by creating a utility class that will help define the mocks. Here's a crude example that takes some of the crazy plumbing in your test and encapsulates it a bit:

public class BookClubContextFixtureHelper
{
    Mock<BookDataService> _mockService;
    ObservableCollection<Category> _categories;

    public BookClubContextFixtureHelper()
    {
        // initialize your context
    }

    public BookDataService Service
    {
       get { return _mockService.Object; }
    }

    public void SetupCategories(param Category[] categories)
    {
         _categories = new ObservableCollection<Category>(categories);

        _mockService
           .Setup( s => s.GetCategories( DefaultInput() )
           .Callback( OnGetCategories )
           .Verifiable();         
    }

    public void VerifyAll()
    {
       _mockService.VerifyAll();
    }

    Action<ObservableCollection<Category>> DefaultInput()
    {
        return It.IsAny<Action<ObservableCollection<Category>>>();
    }

    void OnGetCategories(Action<ObservableCollection<Category>> action)
    {
        action( _categories );
    }
}

However, whenever a test becomes too complicated or needs "advanced" logic, it's often an alarm that something might be wrong. If a ViewModel can't be instantiated because of dependencies that's a deal breaker for me.

In your example, you are creating two dependencies (TestDomain and Context) in order to create your Mock BookDataService. This suggests that although you can create dummy stand-ins for your service you aren't fully decoupled from its implementation.

Few options to consider:

  • You may want to introduce an interface to wrap your existing service. This will definitely solve the viewmodel instantiation problem and may give you an easier to work with API. This however won't solve the back-n-forth logic in your view model.
  • Externalize the loading logic into another testable component. For example, associate your viewmodel with an observer/controller that can listen to the property change event or be notified when new data is needed. You might be able to remove your dataservice as a dependency from the view model altogether.
  • 链接地址: http://www.djcxy.com/p/35336.html

    上一篇: xUnit和Moq不支持异步

    下一篇: Moq:高级模拟设置