【问题标题】:Moq: Advanced Mock Setup起订量:高级模拟设置
【发布时间】:2011-02-14 10:51:16
【问题描述】:

我对 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();
            }
        }

由于这还不够糟糕,您可以看到其中还有另一个 LoadMethod,称为 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];
                }
            }
        }

现在我的模拟设置:

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。 :)

【问题讨论】:

    标签: .net unit-testing moq


    【解决方案1】:

    从起订量的角度来看,您所做的一切在技术上都是正确的。您正在使用 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。这表明尽管您可以为您的服务创建虚拟替身,但您并没有完全与它的实现脱钩。

    需要考虑的选项很少:

    • 您可能需要引入一个接口来包装现有服务。这肯定会解决视图模型实例化问题,并可能让您更轻松地使用 API。然而,这并不能解决视图模型中的来回逻辑。
    • 将加载逻辑外部化到另一个可测试的组件中。例如,将您的视图模型与可以侦听属性更改事件或在需要新数据时收到通知的观察者/控制器相关联。您也许可以将您的数据服务作为依赖项从视图模型中完全移除。

    【讨论】:

    • 非常感谢。我发现了我的错误。还有一件事,会说 mockService.VerifyAll();是 Arrange、Act 或 Assert 单元测试的一部分吗?我在什么阶段验证所有的模拟?
    • VerifyAll 将是一个断言。请注意,并非所有调用都必须是可验证的——这是 Stub 和 Mock 之间的细微差别。 Stub 只是一个模拟对象,被编程为在调用时返回值;如果未达到预期,Mock 可能无法通过测试。
    • 现在我只用两行就明白了 mock 和 Stub 之间的区别。 :) 谢谢。我昨天意识到我在验证 MockObject 时遇到了问题,因为我的 CallBack 期望相应地调用 Action。但由于我无法实例化 SubmitOperation 类,它看起来就像一个死胡同。最初,没有 mock.verifyAll() 感觉不对。但是现在我明白了,如果我根本不关心输入参数,我就不需要它。我需要的是一个简单的存根。
    猜你喜欢
    • 2017-12-13
    • 1970-01-01
    • 1970-01-01
    • 2012-08-26
    • 2010-11-30
    • 1970-01-01
    • 2019-08-19
    • 1970-01-01
    • 2010-11-06
    相关资源
    最近更新 更多