Moq:高级模拟设置

Hou*_*man 3 .net unit-testing moq

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

在我的ViewModel中,ctor正在调用这个load方法:

public void LoadCategories()
        {
            Categories = null;
            BookDataService.GetCategories(GetCategoriesCallback);
        }
Run Code Online (Sandbox Code Playgroud)

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

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

因为这还不够糟糕,正如你所看到的,在调用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];
                }
            }
        }
Run Code Online (Sandbox Code Playgroud)

所以现在我的Mock设置:

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));
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

bry*_*ook 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 );
    }
}
Run Code Online (Sandbox Code Playgroud)

但是,每当测试变得过于复杂或需要"高级"逻辑时,通常会发出警告,表明某些事情可能是错误的.如果ViewModel无法实例化,因为依赖项对我来说是一个交易破坏者.

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

几个选项可供考虑:

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

  • VerifyAll将是一个断言.请注意,并非所有调用都必须是可验证的 - 这是Stub和Mock之间的细微差别.Stub只是一个模拟对象,被编程为在被调用时返回值; 如果不满足期望,模拟可以通过测试. (3认同)