Pull to refresh

Работаем с TypeMock Isolator

Reading time5 min
Views4.1K
Для дотнет-разработчика, планирующего юнит-тестирование, редко встает вопрос о том, что подразумевать под этим пресловутым «юнит»-ом: в подавляющем числе случаев, юнит – это класс, и тем самым любой тест который использует два или более класса юнит тестом не является – это уже интеграционный тест. Здесь мы конечно говорим про наши классы, так как привязка к классам фреймворка или сторонних библиотек – вещь вполне нормальная, и в тестировании не нуждается (хотя как сказать…).

Итак, возникла проблема: как протестировать класс, но вместо других им используемых классов поставить некоторые объекты которые сами по себе ничего не делают (например, не пишут данные в базу), но при этом возвращают ожидаемые значения или же выбрасывают исключения? Решение этой проблемы дают мок-фреймворки, которые помогают нам создать эти хитрые подставные объекты или «моки» (от англ. 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());
  }
}

Вот и все!


Надеюсь в этом коротком посте я показал что моки – это совсем не страшно, и использовать их просто! Спасибо за внимание!

Заметки


  1. Для того, чтобы все это заработало, нужно в нашу сборку добавить ссылки на две другие сборки из GACа – «TypeMock Isolator» и «TypeMock Isolator – Arrange-Act-Assert».
  2. Два других подхода – Reflective Mocks и Natural Mocks – в данном очерке не рассмотрены.
  3. Следует также заметить, что помимо аттрибута Test, нужно использовать аттрибут Isolated либо на уровне метода, либо на уровне класса – этот аттрибут позволяет очистить контекст от используемых мок-объектов.

St. Petersburg ALT.NET Group
Tags:
Hubs:
Total votes 32: ↑20 and ↓12+8
Comments7

Articles