Мок сводит меня с ума от моего последнего проекта. Я недавно обновился до версии 4.0.10827, и я замечаю то, что мне кажется новым поведением.
По сути, когда я вызываю мою макетированную функцию (MakeCall
, в этом примере) в коде, который я тестирую, я передаю объект (TestClass
). Код, который я тестирую, вносит изменения в объект TestClass
до и после вызова MakeCall
. Когда код завершен, я вызываю функцию Verify
в Moq. Я ожидаю, что Moq запишет полный объект, который я передал в MakeCall
, возможно, с помощью механизма, такого как глубокое клонирование. Таким образом, я смогу проверить, что MakeCall
был вызван с точным объектом, с которым я ожидаю, что он будет вызван. К сожалению, это не то, что я вижу.
Я пытаюсь проиллюстрировать это в приведенном ниже коде (надеюсь, немного поясню в процессе).
- Сначала я создаю новый объект
TestClass
. Его свойство Var
установлено на "one"
.
- Затем я создаю смоделированный объект,
mockedObject
, который является моим подопытным.
- Затем я вызываю
MakeCall
метод mockedObject
(кстати, используемая в примере структура Machine.Specifications позволяет читать код в классе When_Testing
сверху вниз).
- Затем я проверяю макет объекта, чтобы убедиться, что он действительно вызывается с
TestClass
со значением Var
"one"
. Это успешно, как я и ожидал.
- Затем я изменяю исходный объект
TestClass
, переназначая свойство Var
на "two"
.
- Затем я пытаюсь проверить, считает ли Мок, что
MakeCall
был вызван с TestClass
со значением "one"
. Это не удается, хотя я ожидаю, что это будет правдой.
- Наконец, я проверяю, считает ли Мок, что
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
};