Moq:高级模拟设置

时间:2011-02-14 10:51:17

标签: .net unit-testing moq

我对Moq来说相对较新,并且有这种复杂的案例可以模拟并且有点困难。我希望有经验的Moq用户可以就此提出建议:

在我的ViewModel中,ctor正在调用此加载方法:

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

我想明显嘲笑服务。但由于服务的方法是无效的,并且返回始终是通过回调,因此对我来说太复杂了。

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

由于这还不够糟糕,正如您所看到的,还有一个名为LoadBooksByCategory()的LoadMethod

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

所以现在我的模拟设置:

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

正如您所看到的,我为每个服务调用创建了两个安装程序,但是由于它们的回调和顺序依赖,它非常容易混淆。

例如,如果viewmodel中的Selectedcategory属性保持为null,则永远不会调用第二个服务调用GetBooksByCategory()。但我唯一可以在这里模拟的就是注入viewmodel的服务。那么我如何通过我的回调影响viewmodel中的内容呢? :)这有意义吗?

最后,我期待ObservableCollection Books被实例化,并且可能填充了一些测试数据(我在这里没有这样做,如果它至少被实例化,我很高兴,所以我可以测试添加一本新书到空集合)

这个想法。一旦我理解了这一点,我想我已经理解了Moq。 :)

1 个答案:

答案 0 :(得分:6)

从Moq的角度来看,您所做的一切在技术上都是正确的。您正在使用Moq的回调机制,该机制通常用于检查入站参数,但在这种情况下,您将调用自定义逻辑来模拟服务的功能。如果将Mocks配置为返回正确的值,则应该能够在演示模型中练习逻辑。您需要使用不同返回值的多个测试来正确执行所有执行路径。在你看来,这会让人感到困惑。

通过创建一个有助于定义模拟的实用程序类,您可以稍微清理一下。这是一个粗略的例子,它在您的测试中采用了一些疯狂的管道并将其封装起来:

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

但是,每当测试变得过于复杂或需要“高级”逻辑时,通常会发出错误警告。如果ViewModel无法实例化,因为依赖性对我来说是一个交易破坏者。

在您的示例中,您要创建两个依赖项(TestDomain和Context)以创建Mock BookDataService。这表明虽然您可以为您的服务创建虚拟替身,但您并没有完全脱离其实施。

几个选项需要考虑:

  • 您可能希望引入一个界面来包装现有服务。这肯定会解决viewmodel实例化问题,并可能使您更容易使用API​​。但是,这不会解决视图模型中的后向逻辑。
  • 将加载逻辑外部化为另一个可测试组件。例如,将viewmodel与可以侦听属性更改事件的观察者/控制器相关联,或者在需要新数据时得到通知。您可以将视图模型中的数据服务作为依赖项删除。