Moq: Расширенная настройка Mock - PullRequest
3 голосов
/ 14 февраля 2011

Я относительно новичок в 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();
            }
        }

Поскольку это было не так уж плохо, как вы можете видеть, есть другой 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];
                }
            }
        }

Так что теперь моя настройка 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));

Как видите, я создал две настройки для каждого служебного вызова, однако из-за их обратного вызова и последовательной зависимости это очень запутанно.

например, второй вызов службы GetBooksByCategory () никогда не будет вызван, если свойство Selectedcategory в viewmodel останется нулевым. Но единственное, что я могу здесь высмеять, это на самом деле только сервис, внедренный в модель представления. Итак, как я собираюсь повлиять на это внутри viewmodel через мой обратный вызов? :) Имеет ли это смысл?

В самом конце я ожидаю, что экземпляры ObservableCollection Books будут инстанцированы и, возможно, заполнены некоторыми тестовыми данными (чего я здесь не делаю, я рад, если они хотя бы инстанцированы, так что я могу протестировать добавление новой книги в пустую коллекцию)

Это идея. Как только я могу понять это, я думаю, что я правильно понимаю Moq. :)

1 Ответ

6 голосов
/ 15 февраля 2011

С точки зрения 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. Это, однако, не решит обратную логику в вашей модели представления.
  • Вывести логику загрузки в другой тестируемый компонент. Например, свяжите вашу модель представления с наблюдателем / контроллером, который может прослушивать событие изменения свойства или получать уведомления, когда требуются новые данные. Возможно, вам удастся полностью удалить службу данных в качестве зависимости от модели представления.
...