Неожиданное подтверждение поведения с Moq - PullRequest
7 голосов
/ 02 апреля 2011

Мок сводит меня с ума от моего последнего проекта. Я недавно обновился до версии 4.0.10827, и я замечаю то, что мне кажется новым поведением.

По сути, когда я вызываю мою макетированную функцию (MakeCall, в этом примере) в коде, который я тестирую, я передаю объект (TestClass). Код, который я тестирую, вносит изменения в объект TestClass до и после вызова MakeCall. Когда код завершен, я вызываю функцию Verify в Moq. Я ожидаю, что Moq запишет полный объект, который я передал в MakeCall, возможно, с помощью механизма, такого как глубокое клонирование. Таким образом, я смогу проверить, что MakeCall был вызван с точным объектом, с которым я ожидаю, что он будет вызван. К сожалению, это не то, что я вижу.

Я пытаюсь проиллюстрировать это в приведенном ниже коде (надеюсь, немного поясню в процессе).

  1. Сначала я создаю новый объект TestClass. Его свойство Var установлено на "one".
  2. Затем я создаю смоделированный объект, mockedObject, который является моим подопытным.
  3. Затем я вызываю MakeCall метод mockedObject (кстати, используемая в примере структура Machine.Specifications позволяет читать код в классе When_Testing сверху вниз).
  4. Затем я проверяю макет объекта, чтобы убедиться, что он действительно вызывается с TestClass со значением Var "one". Это успешно, как я и ожидал.
  5. Затем я изменяю исходный объект TestClass, переназначая свойство Var на "two".
  6. Затем я пытаюсь проверить, считает ли Мок, что MakeCall был вызван с TestClass со значением "one". Это не удается, хотя я ожидаю, что это будет правдой.
  7. Наконец, я проверяю, считает ли Мок, что MakeCall был на самом деле вызван объектом TestClass со значением "two". Это успешно, хотя я первоначально ожидал, что это потерпит неудачу.

Мне кажется довольно ясным, что Moq держит только ссылку на исходный объект TestClass, что позволяет мне безнаказанно изменять его значение, что отрицательно влияет на результаты моего тестирования.

Несколько замечаний по тестовому коду. IMyMockedInterface - интерфейс, над которым я издеваюсь. TestClass - это класс, который я передаю в метод MakeCall и поэтому использую, чтобы продемонстрировать мою проблему. Наконец, When_Testing - это фактический тестовый класс, который содержит тестовый код. Он использует Machine.Specifications framework, поэтому есть несколько странных элементов («Из-за», «Это должно ...»). Это просто делегаты, которые вызываются платформой для выполнения тестов. Они должны быть легко удалены, а содержащийся в них код помещен в стандартную функцию, если это необходимо. Я оставил его в этом формате, потому что он позволяет завершать все вызовы Validate (по сравнению с парадигмой 'Arrange, Act Assert'). Просто чтобы уточнить, приведенный ниже код не является реальным кодом, с которым у меня возникают проблемы. Это просто предназначено, чтобы проиллюстрировать проблему, поскольку я видел такое же поведение в нескольких местах.

using Machine.Specifications;
// Moq has a conflict with MSpec as they both have an 'It' object.
using moq = Moq;

public interface IMyMockedInterface
{
    int MakeCall(TestClass obj);
}

public class TestClass
{
    public string Var { get; set; }

    // Must override Equals so Moq treats two objects with the 
    // same value as equal (instead of comparing references).
    public override bool Equals(object obj)
    {
        if ((obj != null) && (obj.GetType() != this.GetType()))
            return false;
        TestClass t = obj as TestClass;
        if (t.Var != this.Var)
            return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 41;
        int factor = 23;
        hash = (hash ^ factor) * Var.GetHashCode();
        return hash;
    }

    public override string ToString()
    {
        return MvcTemplateApp.Utilities.ClassEnhancementUtilities.ObjectToString(this);
    }
}

[Subject(typeof(object))]
public class When_Testing
{
    // TestClass is set up to contain a value of 'one'
    protected static TestClass t = new TestClass() { Var = "one" };
    protected static moq.Mock<IMyMockedInterface> mockedObject = new moq.Mock<IMyMockedInterface>();
    Because of = () =>
    {
        mockedObject.Object.MakeCall(t);
    };

    // Test One
    // Expected:  Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
    // Actual:  Moq does verify that MakeCall was called with a TestClass with a value of 'one'.
    // Result:  This is correct.
    It should_verify_that_make_call_was_called_with_a_value_of_one = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());

    // Update the original object to contain a new value.
    It should_update_the_test_class_value_to_two = () =>
        t.Var = "two";

    // Test Two
    // Expected:  Moq should verify that MakeCall was called with a TestClass with a value of 'one'.
    // Actual:  The Verify call fails, claiming that MakeCall was never called with a TestClass instance with a value of 'one'.
    // Result:  This is incorrect.
    It should_verify_that_make_call_was_called_with_a_class_containing_a_value_of_one = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "one" }), moq.Times.Once());

    // Test Three
    // Expected:  Moq should fail to verify that MakeCall was called with a TestClass with a value of 'two'.
    // Actual:  Moq actually does verify that MakeCall was called with a TestClass with a value of 'two'.
    // Result:  This is incorrect.
    It should_fail_to_verify_that_make_call_was_called_with_a_class_containing_a_value_of_two = () =>
        mockedObject.Verify(o => o.MakeCall(new TestClass() { Var = "two" }), moq.Times.Once());
}

У меня есть несколько вопросов по этому поводу:

Это ожидаемое поведение?
Это новое поведение?
Есть ли обходной путь, о котором я не знаю?
Я неправильно использую Verify?
Есть ли лучший способ использования Moq, чтобы избежать этой ситуации?

Смиренно благодарю вас за любую помощь, которую вы можете оказать.

Edit:
Вот один из реальных тестов и кода SUT, с которым я столкнулся с этой проблемой. Надеюсь, это послужит разъяснением.

// This is the MVC Controller Action that I am testing.  Note that it 
// makes changes to the 'searchProjects' object before and after 
// calling 'repository.SearchProjects'.
[HttpGet]
public ActionResult List(int? page, [Bind(Include = "Page, SearchType, SearchText, BeginDate, EndDate")] 
    SearchProjects searchProjects)
{
    int itemCount;
    searchProjects.ItemsPerPage = profile.ItemsPerPage;
    searchProjects.Projects = repository.SearchProjects(searchProjects, 
        profile.UserKey, out itemCount);
    searchProjects.TotalItems = itemCount;
    return View(searchProjects);
}


// This is my test class for the controller's List action.  The controller 
// is instantiated in an Establish delegate in the 'with_project_controller' 
// class, along with the SearchProjectsRequest, SearchProjectsRepositoryGet, 
// and SearchProjectsResultGet objects which are defined below.
[Subject(typeof(ProjectController))]
public class When_the_project_list_method_is_called_via_a_get_request
    : with_project_controller
{
    protected static int itemCount;
    protected static ViewResult result;
    Because of = () =>
        result = controller.List(s.Page, s.SearchProjectsRequest) as ViewResult;

    // This test fails, as it is expecting the 'SearchProjects' object 
    // to contain:
    // Page, SearchType, SearchText, BeginDate, EndDate and ItemsPerPage
    It should_call_the_search_projects_repository_method = () =>
        s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsRepositoryGet, 
            s.UserKey, out itemCount), moq.Times.Once());

    // This test succeeds, as it is expecting the 'SearchProjects' object 
    // to contain:
    // Page, SearchType, SearchText, BeginDate, EndDate, ItemsPerPage, 
    // Projects and TotalItems
    It should_call_the_search_projects_repository_method = () =>
        s.Repository.Verify(r => r.SearchProjects(s.SearchProjectsResultGet, 
            s.UserKey, out itemCount), moq.Times.Once());

    It should_return_the_correct_view_name = () =>
        result.ViewName.ShouldBeEmpty();

    It should_return_the_correct_view_model = () =>
        result.Model.ShouldEqual(s.SearchProjectsResultGet);
}


/////////////////////////////////////////////////////
// Here are the values of the three test objects
/////////////////////////////////////////////////////

// This is the object that is returned by the client.
SearchProjects SearchProjectsRequest = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page
};

// This is the object I am expecting the repository method to be called with.
SearchProjects SearchProjectsRepositoryGet = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page, 
    ItemsPerPage = ItemsPerPage
};

// This is the complete object I expect to be returned to the view.
SearchProjects SearchProjectsResultGet = new SearchProjects()
{
    SearchType = SearchTypes.ProjectName,
    SearchText = GetProjectRequest().Name,
    Page = Page, 
    ItemsPerPage = ItemsPerPage,
    Projects = new List<Project>() { GetProjectRequest() },
    TotalItems = TotalItems
};

Ответы [ 2 ]

3 голосов
/ 09 апреля 2011

В конечном счете, ваш вопрос заключается в том, должна ли инфраструктура моделирования делать снимки параметров, которые вы используете при взаимодействии с макетами, чтобы она могла точно записывать состояние, в котором находилась система в точке взаимодействия, а не состояние, в котором могут находиться параметры в точке проверки.

Я бы сказал, что это разумное ожидание с логической точки зрения. Вы выполняете действие X со значением Y. Если вы спросите насмешку «Я выполнил действие X со значением Y», вы ожидаете, что оно скажет «Да» независимо от текущего состояния системы.

Чтобы подвести итог проблемы, с которой вы столкнулись:


  • Сначала вы вызываете метод для фиктивного объекта с параметром ссылочного типа.

  • Moq сохраняет информацию о вызове вместе с передаваемым параметром ссылочного типа.

  • Затем вы спрашиваете Moq, был ли метод вызван один раз с объектом, равным ссылке, которую вы передали.

  • Moq проверяет свою историю для вызова этого метода с параметром, который соответствует предоставленному параметру, и отвечает да.

  • Затем вы изменяете объект, переданный в качестве параметра, для вызова метода в макете.

  • Пространство памяти для ссылки, которую Moq держит в своей истории, меняется на новое значение.

  • Затем вы спрашиваете Moq, был ли метод вызван один раз с объектом, который не равен ссылке, в которой он содержится.

  • Mock проверяет свою историю для вызова этого метода с параметром, который соответствует предоставленному параметру, и сообщает номер.


Чтобы попытаться ответить на ваши конкретные вопросы:

  1. Это ожидаемое поведение?

    Я бы сказал нет.

  2. Это новое поведение?

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

  3. Есть ли обходной путь, о котором я не знаю?

    Я отвечу на это двумя способами.

    С технической точки зрения, обходной путь должен был бы использовать Испытательного Шпиона, а не Ложного. Используя тестового шпиона, вы можете записать переданные значения и использовать свою собственную стратегию для запоминания состояния, например, глубокого клонирования, сериализации объекта или просто сохранения определенных значений, с которыми вы хотите сравнить, с чем позже.

    С точки зрения тестирования, я бы порекомендовал вам следовать принципу «Сначала используйте переднюю дверь» . Я полагаю, что есть время для тестирования на основе состояния, а также тестирования на основе взаимодействия, но вам следует избегать привязки себя к деталям реализации, если взаимодействие не является важной частью сценария. В некоторых случаях интересующий вас сценарий будет главным образом связан с взаимодействием («Перевод средств между счетами»), но в других случаях все, что вас действительно волнует, - это получение правильного результата («Вывод $ 10»). В случае спецификации для вашего контроллера это, похоже, относится к категории запросов, а не к категории команд. Тебя не волнует, как получаются желаемые результаты, если они правильные. Поэтому я бы рекомендовал в этом случае использовать тестирование на основе состояния. Если другая спецификация касается выдачи команды для системы, все равно может оказаться решение для входной двери, которое вы должны рассмотреть в первую очередь, используя его, но может оказаться необходимым или важным провести тестирование на основе взаимодействия. Только мои мысли, хотя.

  4. Я неправильно использую Verify?

    Вы используете метод Verify () правильно, он просто не поддерживает сценарий, для которого вы его используете.

  5. Есть ли лучший способ использования Moq, чтобы избежать этой ситуации?

    Я не думаю, что Moq в настоящее время реализован для обработки этого сценария.

Надеюсь, это поможет,

Дерек Грир
http://derekgreer.lostechies.com
http://aspiringcraftsman.com
@ derekgreer

0 голосов
/ 04 апреля 2011

Во-первых, вы можете избежать конфликта между Moq и MSpec, объявив

using Machine.Specifications;
using Moq;
using It = Machine.Specifications.It;

Тогда вам нужно будет использовать префикс Moq. только тогда, когда вы хотите использовать Moq's It, например Moq.It.IsAny<>().


На ваш вопрос.

Примечание. Это не оригинальный ответ, а отредактированный после того, как ФП добавил некоторый реальный пример кода к вопросу

Я пробовал ваш пример кода и думаю, что он больше связан с MSpec, чем с Moq. Очевидно (и я тоже этого не знал), когда вы изменяете состояние вашей SUT (тестируемой системы) внутри делегата It, изменения запоминаются. То, что происходит сейчас:

  1. Because делегат запущен
  2. It делегаты запускаются один за другим. Если кто-то изменит состояние, следующий It никогда не увидит настройки в Because. Отсюда твой провальный тест.

Я пытался пометить вашу спецификацию SetupForEachSpecificationAttribute:

[Subject(typeof(object)), SetupForEachSpecification]
public class When_Testing
{
    // Something, Something, something... 
}

Атрибут делает так, как его имя говорит: он будет запускать ваши Establish и Because перед каждым It. Добавление атрибута привело к тому, что спецификация работала должным образом: 3 Успеха, один сбой (проверка, что с Var = "two").

Может ли SetupForEachSpecificationAttribute решить вашу проблему или сброс после каждых It не подходит для ваших тестов?

К вашему сведению: я использую Moq v4.0.10827.0 и MSpec v0.4.9.0


Бесплатный совет № 2: Если вы тестируете приложения ASP.NET MVC с помощью Mspec, возможно, вы захотите взглянуть на расширения MSpec Джеймса Брума для MVC

...