Moq:高级模拟设置

3

我相对于Moq比较新,并且有一个复杂的案例需要模拟,但是我有些困惑。我希望有经验的Moq用户能够为我提供建议:

在我的ViewModel中,构造函数调用了这个加载方法:

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

如您所见,我为每个服务调用创建了两个安装程序,但由于它们的回调和顺序依赖性,这非常令人困惑。例如,如果视图模型中的Selectedcategory属性保持为空,则永远不会调用第二个服务调用GetBooksByCategory()。但是,我能够模拟的实际上只是注入到视图模型中的服务。那么我该如何通过我的回调在视图模型内部影响它呢? :) 这有意义吗?
最终,我希望ObservableCollection Books被实例化,并且可能填充一些测试数据(在此处我没有这样做,如果至少实例化了,我将很高兴,以便我可以测试向空集合添加新书籍)
这就是想法。一旦我能理解这一点,我认为我就能正确地理解Moq了。 :)
1个回答

6
从Moq的角度来看,你所做的一切在技术上都是正确的。你正在使用Moq的Callback机制,这通常用于检查入站参数,但在这种情况下,你正在调用自定义逻辑来模拟服务执行的操作。如果配置Mock返回正确的值,你应该能够测试演示模型中的逻辑。你需要几个测试来测试不同的返回值,以正确地测试执行所有路径。对你来说,这会变得混乱。
你可以通过创建一个实用类来帮助定义模拟对象,从而使事情更简洁。以下是一个简单的例子,它将测试中的一些复杂内容封装起来:
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与观察者/控制器相关联,该观察者/控制器可以侦听属性更改事件或在需要新数据时收到通知。您可能能够完全从视图模型中删除数据服务作为依赖项。

非常感谢。我已经找到了我的错误。还有一件事,mockService.VerifyAll(); 是单元测试的安排(Arrange)、行动(Act)还是断言(Assert)的一部分?在哪个阶段我应该验证 mock 的 VerifyAll() 方法? - Houman
3
VerifyAll将成为断言。请注意,并非所有调用都必须可验证--这是存根和模拟之间微妙的区别。存根只是编程为返回值的模拟对象; 如果未满足预期,模拟可以使测试失败。 - bryanbcook
现在我只用两行代码就明白了Mock和Stub的区别。:) 谢谢。昨天我意识到一个问题,我无法验证MockObject,因为我的CallBack期望按照Action<SubmitOperation>的方式调用。但由于我无法实例化SubmitOperation类,看起来像是死路一条。最初感觉没有mock.verifyAll()不对。但现在我明白了,如果我根本不关心输入参数,那么我根本不需要它。我需要的是一个简单的stub。 - Houman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接