Написание тестируемого презентационного уровня с помощью MVVM

OSzone.net » Microsoft » Разработка приложений » Другое » Написание тестируемого презентационного уровня с помощью MVVM
Автор: Брент Эдвардс
Иcточник: MSDN
Опубликована: 20.06.2014

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

Появление Windows Presentation Foundation (WPF) вывело концепцию связывания с данными на совершенно новый уровень. Это позволило развивать новый проектировочный шаблон Model-View-ViewModel (MVVM). MVVM отделяет презентационную логику от собственно представления. В основном, это означает, что по большей части вы можете обходиться без написания кода в файле отделенного кода представления.

Это серьезный шаг вперед для тех, кто заинтересован в разработке тестируемых приложений. Теперь вместо подключения презентационной логики к отделенному коду представления, который имеет свой жизненный цикл, усложняющий тестирование, вы можете использовать POCO-объект (Plain Old CLR Object). У моделей представлений нет ограничений жизненного цикла, которые есть у представлений. Поэтому вы можете просто создать экземпляр модели представления в модульном тесте и проверить ее.

В этой статье я расскажу о подходе к написанию тестируемого презентационного уровня для приложений на основе MVVM. Для иллюстрации этого подхода я буду включать примеры кода из написанной мной инфраструктуры Charmed с открытым исходным кодом и показывать сопутствующее приложение-пример Charmed Reader. Инфраструктура и приложения-примеры доступны на GitHub по ссылке github.com/brentedwards/Charmed.

Я ознакомил вас с инфраструктурой Charmed для Windows 8 и с приложением-примером в своей статье за июль 2013 г. (msdn.microsoft.com/magazine/dn296512). Затем в статье за сентябрь 2013 г. (msdn.microsoft.com/magazine/dn385706) я рассказал, как сделать ее кросс-платформенной инфраструктурой для Windows 8 и Windows Phone 8 (то же самое относилось и к приложению-примеру). В обеих статьях я говорил о решениях, принимавшихся мной для сохранения тестируемости приложения. Теперь я вернусь к этим решениям и покажу, как я на самом деле собираюсь тестировать приложение. В этой статье используется код приложения-примера, написанный для Windows 8 и Windows Phone 8, но концепции и методики применимы к приложению любого типа.

О приложении-примере

Приложение-пример, иллюстрирующее мой подход к написанию тестируемого презентационного уровня, называется Charmed Reader. Это простое приложение для чтения блогов, которое работает как в Windows 8, так и в Windows Phone 8. Оно обладает минимальной функциональностью для демонстрации ключевых моментов, на которые я хочу обратить внимание. Оно является кросс-платформенным и по большей части работает одинаково на обеих платформах с тем исключением, что в Windows 8 используется некоторая функциональность, специфичная для Windows 8. Хотя это базовое приложение, оно достаточно функционально для модульного тестирования.

Что такое модульное тестирование?

Идея, стоящая за модульным тестированием, заключается в том, чтобы брать дискретные блоки кода (модули) и писать методы теста, которые используют код ожидаемым образом, а затем смотреть, дают ли они ожидаемые результаты. Код теста выполняется с применением какого-либо вида инфраструктуры тестирования. С Visual Studio 2012 работает несколько таких инфраструктур. В коде примера я использую MSTest, встроенную в Visual Studio 2012 (и более ранние версии). Цель — создание метода теста модуля, ориентированного на конкретный сценарий. Иногда для охвата всех сценариев в рамках тестируемого модуля требуется несколько методов теста.

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

  1. Arrange (подготовка).
  2. Act (выполнение).
  3. Assert (проверка).

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

Во-вторых, после завершения подготовительной части к собственно тесту вы можете выполнить интересующий вас метод или свойство. Это раздел Act теста. Вы можете выполнять метод или свойство с параметрами (если это применимо), подготовленными в разделе Arrange.

Наконец, когда вы выполнили интересующий вас метод или свойство, тест должен проверить, что этот метод или свойство сделали именно то, что от него ожидалось. Это раздел Assert теста. В фазе Assert вызываются методы проверки для сравнения полученных результатов с ожидаемыми. Если полученные результаты соответствуют ожидаемым, модульный тест пройден. В ином случае тест провален.

Следуя этому формату, мои тесты обычно выглядят примерно так:

[TestMethod]
public void SomeTestMethod()
{
  // Arrange
  // *Вставка кода для подготовки теста
  // Act
  // *Вставка кода для вызова тестируемого метода или свойства
  // Assert
  // *Вставка кода для проверки завершения теста
  // ожидаемым образом
}

Некоторые используют этот формат, не включая комментарии для обозначения различных разделов теста (Arrange/Act/Assert). Я предпочитаю разделять их комментариями, чтобы не забыть, что именно делает тест.

Дополнительное преимущество создания обширного набора тщательно написанных модульных тестов заключается в том, что они служат своего рода «активной» документацией приложения. Новые разработчики, изучающие ваш код, смогут понять, какое использование кода вы ожидаете, просмотрев различные сценарии в модульных тестах.

Планирование с учетом тестируемости

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

Приложение Charmed Reader позволяет читать публикации в блогах. Скачивание этих публикаций включает веб-доступ к RSS-каналам, и эта функциональность может оказаться довольно трудной в модульном тестировании. Прежде всего у вас должна быть возможность быстро и в отключенном состоянии выполнять модульные тесты. Опора на веб-доступ в модульном тесте потенциально грозит нарушением этих принципов.

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

Мне было известно, что должно происходить в приложении.

  1. MainViewModel должен загружать все публикации из блогов, которые пользователь хочет прочитать сразу.
  2. Эти публикации нужно скачивать из разных RSS-каналов, сохраненных пользователем.
  3. После скачивания публикации нужно разобрать в объекты передачи данных (data transfer objects, DTO) и сделать доступными представлению.

Если бы я поместил код для скачивания данных из RSS-каналов в MainViewModel, он вдруг стал бы ответственным за нечто большее простой загрузки данных и привязки представления через связывание с данными для отображения. Тогда MainViewModel отвечал бы за выдачу веб-запросов и разбор XML-данных. Поэтому для таких целей MainViewModel должна обращаться к вспомогательным объектам. Далее MainViewModel должна получать экземпляры объектов, представляющие отображаемые публикации из блогов. Эти объекты называются DTO.

Зная это, я могу абстрагировать загрузку RSS-канала и разбор во вспомогательный объект, вызываемый MainViewModel. Однако это еще не все. Если бы я просто создал вспомогательный класс, который выполняет работы с данными RSS-каналов, то любой модульный тест, написанный для MainViewModel и проверяющий эту функциональность, в конечном счете тоже вызывал бы этот вспомогательный класс для веб-доступа. Как уже упоминалось, это идет вразрез с целями модульного тестирования. Таким образом, нужно было сделать еще один шаг.

Если же я создам интерфейс для работы с RSS-каналами, модель представления сможет работать с интерфейсом вместо конкретного класса. Тогда я смогу предоставлять другие реализации этого интерфейса при выполнении модульных тестов. Эта концепция называется имитацией (mocking). Когда я запускаю приложение для реальной работы, мне нужен реальный объект, который загружает реальные данные из RSS-каналов. Но, когда я запускаю модульные тесты, мне нужен имитирующий объект, который просто делает вид, будто он загружает RSS-данные, а на самом деле никогда не выходит в Web. Имитирующий объект может создавать согласованные данные, которые являются воспроизводимыми и никогда не изменяются. В таком случае модульные тесты будут точно знать, что ожидать при каждом прогоне.

С учетом этого мой интерфейс для загрузки публикаций из блогов выглядит так:

public interface IRssFeedService
{
  Task<List<FeedData>> GetFeedsAsync();
}

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

Windows 8 и Windows Phone 8 по-разному скачивают и разбирают данные RSS-каналов. Создав интерфейс IRssFeedService и заставив MainViewModel взаимодействовать с ним, а не напрямую скачивать данные из RSS-каналов, я избегаю необходимости в нескольких реализациях одной и той же функциональности для MainViewModel.

Используя встраивание зависимости, я могу гарантированно предоставлять MainViewModel корректный экземпляр IRssFeedService в нужное время. Как уже упоминалось, при выполнении модульных тестов я подсовываю имитирующий экземпляр IRssFeedService. Кстати, любопытно, что сейчас для платформ Windows 8 и Windows Phone 8 нет динамической инфраструктуры имитации. Поскольку имитация составляет большую часть того, как я выполняю модульные тесты для своего кода, мне пришлось придумать собственный способ создания имитирующих заглушек. Полученный RssFeedServiceMock показан на рис. 1.

Рис. 1. RssFeedServiceMock

public class RssFeedServiceMock : IRssFeedService
{
  public Func<List<FeedData>> GetFeedsAsyncDelegate { get; set; }
  public Task<List<FeedData>> GetFeedsAsync()
  {
    if (this.GetFeedsAsyncDelegate != null)
    {
      return Task.FromResult<List<FeedData>>(this.GetFeedsAsyncDelegate());
    }
    else
    {
      return Task.FromResult<List<FeedData>>(null);
    }
  }
}

По сути, мне нужна возможность предоставлять делегат, который способен настраивать то, как загружаются данные. Если вы ведете разработку не для Windows 8 или Windows Phone 8, то можете использовать динамические имитирующие инфраструктуры, такие как Moq, Rhino Mocks или NSubstitute. Независимо от того, применяете вы собственные имитации или динамическую имитирующую инфраструктуру, принципы те же.

Теперь, когда я создал интерфейс IRssFeedService, встроил его в MainViewModel, который вызывает GetFeedsAsync в этом интерфейсе, а также создал RssFeedServiceMock, пора выполнить модульный тест взаимодействия MainViewModel с IRssFeedService. В этом взаимодействии важно проверить, что MainViewModel корректно вызывает GetFeedsAsync и что возвращаемые данные каналов идентичны тем, которые MainViewModel делает доступными через свойство FeedData. Модульный тест на рис. 2 подтверждает это.

Рис. 2. Тестирование функциональности загрузки каналов

[TestMethod]
public void FeedData()
{
  // Arrange
  var viewModel = GetViewModel();
  var expectedFeedData = new List<FeedData>();
  this.RssFeedService.GetFeedsAsyncDelegate = () =>
    {
      return expectedFeedData;
    };
  // Act
  var actualFeedData = viewModel.FeedData;
  // Assert
  Assert.AreSame(expectedFeedData, actualFeedData);
}

Всякий раз, когда я выполняю модульный тест модели представления (или любого другого объекта, если это имеет какое-то значение), я предпочитаю располагать вспомогательным методом, который дает мне экземпляр тестируемой модели представления. Со временем модели представления скорее всего будут изменяться, в частности в модель представления могут вводиться другие вещи, а значит, изменятся параметры конструктора. Если я создаю новый экземпляр модели представления во всех своих модульных тестах, а затем изменяю сигнатуру конструктора, то вынужден попутно модифицировать уйму модульных тестов. Однако, если я пишу вспомогательный метод для создания этого экземпляра модели представления, то должен внести изменения только в одном месте. В данном случае GetViewModel является таким вспомогательным методом:

private MainViewModel GetViewModel()
{
  return new MainViewModel(this.RssFeedService,
    this.Navigator, this.MessageBus);
}

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

[TestInitialize]
public void Init()
{
  this.RssFeedService = new RssFeedServiceMock();
  this.Navigator = new NavigatorMock();
  this.MessageBus = new MessageBusMock();
}

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

Вспомним сам тест. Следующий код создает мои ожидаемые данные каналов и подготавливает имитатор сервиса RSS-каналов на возврат этих данных:

var expectedFeedData = new List<FeedData>();
this.RssFeedService.GetFeedsAsyncDelegate = () =>
  {
    return expectedFeedData;
  };

Заметьте, что я не добавляю никаких реальных экземпляров FeedData к списку expectedFeedData, поскольку в этом нет нужды. Мне требуется лишь гарантировать, что сам список содержит то же, что и MainViewModel в конечном счете. Меня не волнует, что происходит, когда в списке действительно появляются экземпляры FeedData — по крайней мере, в этом тесте.

В разделе Act теста есть такая строка:

var actualFeedData = viewModel.FeedData;

Затем я могу проверить, что actualFeedData идентичен экземпляру expectedFeedData. Если они не идентичны, значит, MainViewModel не сделал свою работу и модульный тест должен быть провален:

Assert.AreSame(expectedFeedData, actualFeedData);

Создание тестируемой навигации

Другая важная часть приложения-примера, которую мне нужно тестировать, — навигация. Charmed Reader использует навигацию на основе модели представления, так как я хочу сохранить отделение представлений от их моделей. Charmed Reader — кросс-платформенное приложение, и создаваемые мной модели представлений используются на обеих платформах, хотя представления для Windows 8 и Windows Phone 8 должны быть разными. Существует ряд причин этому, но, по сути, все сводится к тому, что каждая из этих платформ использует слегка различающийся XAML. Из-за этого я и не хотел, чтобы модели представлений знали о представлениях, запутывая общую картину.

Я решил абстрагировать функционал навигации за интерфейсом по нескольким причинам. Первая и самая важная — на каждой платформе свои классы, участвующие в навигации, а я не хотел, чтобы модель представления имела дело с этими различиями. Кроме того, в обоих случаях классы, участвующие в навигации, нельзя имитировать. Поэтому я абстрагировал эти проблемы от модели представления и создал интерфейс INavigator:

public interface INavigator
{
  bool CanGoBack { get; }
  void GoBack();
  void NavigateToViewModel<TViewModel>(object parameter = null);
#if WINDOWS_PHONE
  void RemoveBackEntry();
#endif // WINDOWS_PHONE
}

Я встроил INavigator в MainViewModel через конструктор, и MainViewModel использует INavigator в методе ViewFeed:

public void ViewFeed(FeedItem feedItem)
{
  this.navigator.NavigateToViewModel<FeedItemViewModel>(feedItem);
}

Глядя на то, как ViewFeed взаимодействует с INavigator, я вижу две вещи, которые хочу проверить в модульном тесте.

  1. FeedItem, передаваемый в ViewFeed, идентичен FeedItem, передаваемому в NavigateToViewModel.
  2. Тип модели представления, передаваемый NavigateToViewModel, является FeedItemViewModel.

Прежде чем писать тест, нужно создать еще одну имитацию, на этот раз для INavigator (рис. 3). Я следовал тому же шаблону, что и ранее с делегатами для каждого метода: код теста выполняется при вызове реального метода. И вновь, если вы работаете на платформе с поддержкой инфраструктуры имитации, вам не придется создавать собственную имитацию.

Рис. 3. Имитация для INavigator

public class NavigatorMock : INavigator
{
  public bool CanGoBack { get; set; }
  public Action GoBackDelegate { get; set; }
  public void GoBack()
  {
    if (this.GoBackDelegate != null)
    {
      this.GoBackDelegate();
    }
  }
  public Action<Type, object> NavigateToViewModelDelegate { get; set; }
  public void NavigateToViewModel<TViewModel>(object parameter = null)
  {
    if (this.NavigateToViewModelDelegate != null)
    {
      this.NavigateToViewModelDelegate(typeof(TViewModel), parameter);
    }
  }
#if WINDOWS_PHONE
  public Action RemoveBackEntryDelegate { get; set; }
  public void RemoveBackEntry()
  {
    if (this.RemoveBackEntryDelegate != null)
    {
      this.RemoveBackEntryDelegate();
    }
  }
#endif // WINDOWS_PHONE
}

Теперь, создав имитирующий класс Navigator, я могут поместить его в модульный тест и использовать, как показано на рис. 4.

Рис. 4. Тестирование навигации с применением имитирующего Navigator

[TestMethod]
public void ViewFeed()
{
  // Arrange
  var viewModel = this.GetViewModel();
  var expectedFeedItem = new FeedItem();
  Type actualViewModelType = null;
  FeedItem actualFeedItem = null;
  this.Navigator.NavigateToViewModelDelegate = (viewModelType, parameter) =>
    {
      actualViewModelType = viewModelType;
      actualFeedItem = parameter as FeedItem;
    };
  // Act
  viewModel.ViewFeed(expectedFeedItem);
  // Assert
  Assert.AreSame(expectedFeedItem, actualFeedItem, "FeedItem");
  Assert.AreEqual(typeof(FeedItemViewModel),
    actualViewModelType, "ViewModel Type");
}

В этом тесте меня по-настоящему интересует корректность передаваемого FeedItem, а также правильность модели представления, по которой происходит навигация. При работе с имитациями важно учитывать, что вас заботит в конкретном тесте, а что — нет. В данном тесте, поскольку у меня есть интерфейс INavigator, с которым работает MainViewModel, мне незачем волноваться о том, происходит ли вообще навигация. Это обрабатывается тем, что реализует INavigator для экземпляра периода выполнения. Мне надо лишь убедиться, что INavigator получает должные параметры при навигации.

Создание тестируемых дополнительных плиток

Последнее, что я хотел бы рассмотреть, — дополнительные плитки (secondary tiles). Они доступны как в Windows 8, так и в Windows Phone 8, и позволяют закреплять элементы приложения на начальном экране, создавая внешнюю ссылку (deep link) на конкретную часть приложения. Однако дополнительные плитки обрабатываются на этих платформах совершенно по-разному, а значит, я вынужден предоставлять реализации, специфичные для платформ. Несмотря на различия, можно создать согласованный интерфейс для дополнительных плиток, который удастся использовать на обеих платформах:

public interface ISecondaryPinner
{
  Task<bool> Pin(TileInfo tileInfo);
  Task<bool> Unpin(TileInfo tileInfo);
  bool IsPinned(string tileId);
}

Класс TileInfo — DTO с объединенными свойствами для обеих платформ, предназначенный для создания дополнительной плитки. Поскольку на каждой платформе используется своя комбинация свойств из TileInfo, требуется раздельное тестирование. Я рассмотрю версию для Windows 8. На рис. 5 показано, как моя модель представления использует ISecondaryPinner.

Рис. 5. Применение ISecondaryPinner

public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
{
  // Закрепляем элемент канала, затем локально сохраняем его,
  // чтобы убедиться, что он по-прежнему доступен при возврате
  var tileInfo = new TileInfo(
    this.FormatSecondaryTileId(),
    this.FeedItem.Title,
    this.FeedItem.Title,
    Windows.UI.StartScreen.TileOptions.ShowNameOnLogo |
      Windows.UI.StartScreen.TileOptions.ShowNameOnWideLogo,
    new Uri("ms-appx:///Assets/Logo.png"),
    new Uri("ms-appx:///Assets/WideLogo.png"),
    anchorElement,
    Windows.UI.Popups.Placement.Above,
    this.FeedItem.Id.ToString());
  this.IsFeedItemPinned = await this.secondaryPinner.Pin(tileInfo);
  if (this.IsFeedItemPinned)
  {
    await SavePinnedFeedItem();
  }
}

В методе Pin на рис. 5 по большому счету происходят две вещи. Первое — закрепление дополнительной плитки и второе — сохранение FeedItem в локальном хранилище. А значит, тестировать я должен именно это. Так как данный метод изменяет свойство IsFeedItemPinned в модели представления на основе результатов попытки закрепления FeedItem, мне также нужно проверить два возможных результата метода Pin в ISecondaryPinner: true и false. На рис. 6 показан первый тест, проверяющий успешный вариант.

Рис. 6. Проверка на успешное закрепление

[TestMethod]
public async Task Pin_PinSucceeded()
{
  // Arrange
  var viewModel = GetViewModel();
  var feedItem = new FeedItem
  {
    Title = Guid.NewGuid().ToString(),
    Author = Guid.NewGuid().ToString(),
    Link = new Uri("http://www.bing.com")
  };
  viewModel.LoadState(feedItem, null);
  Placement actualPlacement = Placement.Default;
  TileInfo actualTileInfo = null;
  SecondaryPinner.PinDelegate = (tileInfo) =>
    {
      actualPlacement = tileInfo.RequestPlacement;
      actualTileInfo = tileInfo;
      return true;
    };
  string actualKey = null;
  List<FeedItem> actualPinnedFeedItems = null;
  Storage.SaveAsyncDelegate = (key, value) =>
    {
      actualKey = key;
      actualPinnedFeedItems = (List<FeedItem>)value;
    };
  // Act
  await viewModel.Pin(null);
  // Assert
  Assert.AreEqual(Placement.Above, actualPlacement, "Placement");
  Assert.AreEqual(string.Format(Constants.SecondaryIdFormat,
    viewModel.FeedItem.Id), actualTileInfo.TileId, "Tile Info Tile Id");
  Assert.AreEqual(viewModel.FeedItem.Title,
    actualTileInfo.DisplayName, "Tile Info Display Name");
  Assert.AreEqual(viewModel.FeedItem.Title,
    actualTileInfo.ShortName, "Tile Info Short Name");
  Assert.AreEqual(viewModel.FeedItem.Id.ToString(),
    actualTileInfo.Arguments, "Tile Info Arguments");
  Assert.AreEqual(Constants.PinnedFeedItemsKey, actualKey, "Save Key");
  Assert.IsNotNull(actualPinnedFeedItems, "Pinned Feed Items");
}

В этом тесте подготовки требуется больше, чем в предыдущих. После контроллера я подготавливаю экземпляр FeedItem. Заметьте, что я вызываю ToString в Guids как для Title, так и для Author. Дело в том, что меня не интересуют конкретные значения, а лишь сам факт их наличия, чтобы их можно было сравнить с теми, которые содержатся в разделе Assert. Поскольку Link — это Uri, мне требуется допустимый Uri, чтобы этот тест работал. И вновь само значение Uri не имеет значения — важно лишь то, что оно допустимо. Остальная часть подготовки заключается в захвате взаимодействий для закрепления и сохранения с целью сравнения в разделе Assert. Главный индикатор того, что этот код реально проверяет успешный вариант, — PinDelegate возвращает true, сообщая об успехе.

На рис. 7 приведен во многом тот же тест, но для неудачного варианта. Тот факт, что PinDelegate возвращает false, гарантирует, что тест сфокусирован на проверке неудачного варианта. В этом случае я должен быть уверен, что при неудаче не вызывается SaveAsync в разделе Assert.

Рис. 7. Проверка на неудачное закрепление

[TestMethod]
public async Task Pin_PinNotSucceeded()s
{
  // Arrange
  var viewModel = GetViewModel();
  var feedItem = new FeedItem
  {
    Title = Guid.NewGuid().ToString(),
    Author = Guid.NewGuid().ToString(),
    Link = new Uri("http://www.bing.com")
  };
  viewModel.LoadState(feedItem, null);
  Placement actualPlacement = Placement.Default;
  TileInfo actualTileInfo = null;
  SecondaryPinner.PinDelegate = (tileInfo) =>
  {
    actualPlacement = tileInfo.RequestPlacement;
    actualTileInfo = tileInfo;
    return false;
  };
  var wasSaveCalled = false;
  Storage.SaveAsyncDelegate = (key, value) =>
  {
    wasSaveCalled = true;
  };
  // Act
  await viewModel.Pin(null);
  // Assert
  Assert.AreEqual(Placement.Above, actualPlacement, "Placement");
  Assert.AreEqual(string.Format(Constants.SecondaryIdFormat,
    viewModel.FeedItem.Id), actualTileInfo.TileId, "Tile Info Tile Id");
  Assert.AreEqual(viewModel.FeedItem.Title, actualTileInfo.DisplayName,
    "Tile Info Display Name");
  Assert.AreEqual(viewModel.FeedItem.Title, actualTileInfo.ShortName,
    "Tile Info Short Name");
  Assert.AreEqual(viewModel.FeedItem.Id.ToString(),
    actualTileInfo.Arguments, "Tile Info Arguments");
  Assert.IsFalse(wasSaveCalled, "Was Save Called");
}

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

В рамках этих трех статей я обсудил написание тестируемых приложений на основе шаблона MVVM для Windows 8 и Windows Phone 8. В первой статье было рассмотрено написание тестируемых приложений Windows 8, хотя использование специфических средств Windows 8 по-прежнему не так-то просто охватить тестами. Во второй статье обсуждалась разработка тестируемых кросс-платформенных приложений для Windows 8 и Windows Phone 8. В этой статье был показан мой подход к тестированию приложений.

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


Ссылка: http://www.oszone.net/24356/MVVM