Для дотнет-разработчика, планирующего юнит-тестирование, редко встает вопрос о том, что подразумевать под этим пресловутым «юнит»-ом: в подавляющем числе случаев, юнит – это класс, и тем самым любой тест который использует два или более класса юнит тестом не является – это уже интеграционный тест. Здесь мы конечно говорим про наши классы, так как привязка к классам фреймворка или сторонних библиотек – вещь вполне нормальная, и в тестировании не нуждается (хотя как сказать…).
Итак, возникла проблема: как протестировать класс, но вместо других им используемых классов поставить некоторые объекты которые сами по себе ничего не делают (например, не пишут данные в базу), но при этом возвращают ожидаемые значения или же выбрасывают исключения? Решение этой проблемы дают мок-фреймворки, которые помогают нам создать эти хитрые подставные объекты или «моки» (от англ. mock — копия, имитация). Давайте воспользуемся библиотекой TypeMock и посмотрим, как же работают эти «моки» в действии.
Что нам нужно для использования моков? Три вещи – Visual Studio, TypeMock, и подходящий фреймворк для юнит-тестирования (я воспользуюсь MbUnit). Ну и желание все это изучать.
Сначала небольшой сценарий. Допустим что у меня есть система учета выработок которая считает, сколько я должен работнику. Выглядит она примерно так:
У нас есть некий класс Worker (рабочий), который производит определенное количество работы, и эти часы складываются в массив. Система выплат – Payroll – получает сумму этих часов, умножает их на почасовой рейт, ну и делает выплаты. Теперь представим, что мы хотим тестировать класс
Теперь наш работник как бы и работник, но уже с некой конфигурируемой начинкой[1]. Теперь мы хотим чтобы подсчет прошел, но чтобы реальный
Все просто – вместо последующих вызовов
То что мы сейчас сделали – это подготовка (Arrange phase) – первая из трех фаз методологии Arrange-Act-Assert (AAA) которую поддерживает TypeMock[2]. Сейчас займемся второй фазой – Act. Тут мы собственно вызовем наш метод, то есть проведем действие над тестируемой системой:
А теперь попробуйте угадать результат! Ведь мы даже
Напоследок, посмотрим на весь тест целиком[3]
Итак, что же мы сделали? Мы протестировали метод
В нашем первом примере все было очень просто – все наши элементы были публичны и поэтому проблем с доступом не возникло. А теперь представим себе, что метод
Хьюстон, у нас проблема! Тест наш больше не скомпилится, т.к. две строчки кода использующие
Как подменить непубличный метод? Элементарно, Ватсон! Используя
Вот и все! Точно так же как и методы, можно перехватывать обращения, например, к свойству или индексатору (
Помимо работы с объектами, которые можно создать оператором
Помимо подмены объектов моками, TypeMock поддерживает утипизацию (duck typing), то есть возможность подмены одного объекта другим даже когда у них не совсем одинаковые интерфейсы. Вот небольшой пример:
Тут у нас утка и собака, и мы естественно хотим чтобы собака крякала. В TypeMock это делается так:
Надеюсь в этом коротком посте я показал что моки – это совсем не страшно, и использовать их просто! Спасибо за внимание!
Итак, возникла проблема: как протестировать класс, но вместо других им используемых классов поставить некоторые объекты которые сами по себе ничего не делают (например, не пишут данные в базу), но при этом возвращают ожидаемые значения или же выбрасывают исключения? Решение этой проблемы дают мок-фреймворки, которые помогают нам создать эти хитрые подставные объекты или «моки» (от англ. mock — копия, имитация). Давайте воспользуемся библиотекой TypeMock и посмотрим, как же работают эти «моки» в действии.
Что нам нужно для использования моков? Три вещи – Visual Studio, TypeMock, и подходящий фреймворк для юнит-тестирования (я воспользуюсь MbUnit). Ну и желание все это изучать.
Первые Шаги
Сначала небольшой сценарий. Допустим что у меня есть система учета выработок которая считает, сколько я должен работнику. Выглядит она примерно так:
public class Worker { private List<int> workHours { get; set; } public int GetTotalHoursWorked() { return workHours.Sum(); } } public class Payroll { public int CalculatePay(Worker worker) { // pay everyone 10 dollars an hour (I am evil) return worker.GetTotalHoursWorked() * 10; } }
У нас есть некий класс Worker (рабочий), который производит определенное количество работы, и эти часы складываются в массив. Система выплат – Payroll – получает сумму этих часов, умножает их на почасовой рейт, ну и делает выплаты. Теперь представим, что мы хотим тестировать класс
Payroll
, но чтобы это был настоящий юнит-тест, нужно изолировать зависимый класс Person
так, чтобы например функция GetTotalHoursWorked()
не вызывалась совсем. Как это сделать? Очень просто: Сначала, создаем Payroll
как обычно, а вот вместо Person
создаем мок-объект:Worker w = Isolate.Fake.Instance<Worker>(); Payroll p = new Payroll();
Теперь наш работник как бы и работник, но уже с некой конфигурируемой начинкой[1]. Теперь мы хотим чтобы подсчет прошел, но чтобы реальный
Worker
при этом не затрагивался. Для этого, нужно подменить вызов GetTotalHoursWorked()
. Вот как это делается:Isolate.WhenCalled(() => w.GetTotalHoursWorked()).WillReturn(40);
Все просто – вместо последующих вызовов
Person.GetTotalHoursWorked()
будет банально возвращаться число 40. Если не верите – поставьте брейкпоинт на функцию, и вы убедитесь что при тестировании никто в нее не входит.То что мы сейчас сделали – это подготовка (Arrange phase) – первая из трех фаз методологии Arrange-Act-Assert (AAA) которую поддерживает TypeMock[2]. Сейчас займемся второй фазой – Act. Тут мы собственно вызовем наш метод, то есть проведем действие над тестируемой системой:
int result = p.CalculatePay(w);
А теперь попробуйте угадать результат! Ведь мы даже
workHours
не инициализировали – там значение null
. Тем не менее, у нас вполне реальный результат – 400. Более того, мы можем даже проверить что метод GetTotalHoursWorked()
был действительно вызван (то, что был вызван подмененный метод значения не имеет). Это – последняя фаза ААА, а именно Assert. Смотрим:Assert.AreEqual(400, result); Isolate.Verify.WasCalledWithAnyArguments(() => w.GetTotalHoursWorked());
Напоследок, посмотрим на весь тест целиком[3]
[Test] public void TestPayroll() { // Arrange Payroll p = new Payroll(); Worker w = Isolate.Fake.Instance<Worker>(); Isolate.WhenCalled(() => w.GetTotalHoursWorked()).WillReturn(40); // Act int result = p.CalculatePay(w); // Assert Assert.AreEqual(400, result); Isolate.Verify.WasCalledWithAnyArguments(() => w.GetTotalHoursWorked()); }
Итак, что же мы сделали? Мы протестировали метод
Payroll.CalculatePay()
, подменив параметр Person
неким подобием, которое вело себя предсказуемо и не затрагивало при этом реальные свойства и методы класса.NonPublic
В нашем первом примере все было очень просто – все наши элементы были публичны и поэтому проблем с доступом не возникло. А теперь представим себе, что метод
Worker.GetTotalHoursWorked()
находится в другой сборке, и помечен как internal
:public class Worker { private List<int> workHours { get; set; } internal int GetTotalHoursWorked() { return workHours.Sum(); } }
Хьюстон, у нас проблема! Тест наш больше не скомпилится, т.к. две строчки кода использующие
GetTotalHoursWorked()
больше не имеют к нему доступ:// не сработает Isolate.WhenCalled(() => w.GetTotalHoursWorked()).WillReturn(40); // и это тоже Isolate.Verify.WasCalledWithAnyArguments(() => w.GetTotalHoursWorked());
Как подменить непубличный метод? Элементарно, Ватсон! Используя
Isolator.NonPublic
мы можем задать метод по имени:Isolate.NonPublic.WhenCalled(w, "GetTotalHoursWorked").WillReturn(40); ... Isolate.Verify.NonPublic.WasCalled(w, "GetTotalHoursWorked");
Вот и все! Точно так же как и методы, можно перехватывать обращения, например, к свойству или индексатору (
operator this[]
). Ну и проверки на вызовы можно делать соотвественно.Статики, утипизация, и прочее
Помимо работы с объектами, которые можно создать оператором
new
, TypeMock также умеет работать со статическими объектами. Например, чтобы подделать статический конструктор, мы просто вызываем Isolate.Fake.StaticConstructor(typeof (T));
, а дальше пользуемся TypeMock как и ранее. То же самое делается со статичными методами.Помимо подмены объектов моками, TypeMock поддерживает утипизацию (duck typing), то есть возможность подмены одного объекта другим даже когда у них не совсем одинаковые интерфейсы. Вот небольшой пример:
public class Dog { public Dog(){} public string MakeSound() { return "Woof"; } } public class Duck { public string MakeSound() { return "Quack"; } }
Тут у нас утка и собака, и мы естественно хотим чтобы собака крякала. В TypeMock это делается так:
[TestFixture, Isolated] public class Tests { [Test] public void Test() { // fake a dog Dog dog = Isolate.Fake.Instance<Dog>(); Duck duck = new Duck(); // replace calls on dog with calls on duck Isolate.Swap.CallsOn(dog).WithCallsTo(duck); // get a dog to quack string sound = dog.MakeSound(); // did it? Assert.AreEqual("Quack", sound); Isolate.Verify.WasCalledWithAnyArguments(() => dog.MakeSound()); } }
Вот и все!
Надеюсь в этом коротком посте я показал что моки – это совсем не страшно, и использовать их просто! Спасибо за внимание!
Заметки
- ↑ Для того, чтобы все это заработало, нужно в нашу сборку добавить ссылки на две другие сборки из GACа – «TypeMock Isolator» и «TypeMock Isolator – Arrange-Act-Assert».
- ↑ Два других подхода – Reflective Mocks и Natural Mocks – в данном очерке не рассмотрены.
- ↑ Следует также заметить, что помимо аттрибута
Test
, нужно использовать аттрибутIsolated
либо на уровне метода, либо на уровне класса – этот аттрибут позволяет очистить контекст от используемых мок-объектов.