MVVM

Criando uma camada de apresentação que pode ser testada com o MVVM

Brent Edwards

Baixar o código de exemplo

Com os aplicativos tradicionais da época do Windows Forms, a prática padrão para testes era dispor o layout de uma exibição, escrever o código no code-behind da exibição e executar o aplicativo como um teste. Felizmente, as coisas evoluíram um pouco desde então.

O advento do WPF (Windows Presentation Foundation) levou o conceito da vinculação de dados a um nível completamente novo. O WPF permitiu a evolução de um novo padrão de design chamado MVVM (Model-View-ViewModel). O MVVM permite separar a lógica da apresentação da apresentação real. Basicamente, isso significa que, na maior parte das vezes, você pode evitar escrever código no code-behind da exibição.

Essa é uma melhoria importante para os interessados em desenvolver aplicativos que podem ser testados. Agora, em vez de usar a lógica da apresentação anexada ao code-behind da exibição, que tem seu próprio ciclo de vida para complicar os testes, você pode usar um POCO (objeto CLR básico). Os modelos de exibição não precisam ter as restrições de ciclo de vida que uma exibição tem. Você pode apenas criar uma instância de um modelo de exibição em um teste de unidade e testar.

Neste artigo, examinarei como abordar a criação de uma camada da apresentação que pode ser testada para aplicativos que usam o MVVM. Para ajudar a ilustrar minha abordagem, incluirei um código de exemplo de uma estrutura de software livre que escrevi, chamada Charmed, e um aplicativo de exemplo de acompanhamento, chamado Charmed Reader. A estrutura e os aplicativos de exemplo estão disponíveis no GitHub em github.com/brentedwards/Charmed.

Apresentei a estrutura do Charmed em meu artigo de julho de 2013 (msdn.microsoft.com/magazine/dn296512) como uma estrutura e um aplicativo de exemplo do Windows 8. Em seguida, em meu artigo de setembro de 2013 (msdn.microsoft.com/magazine/dn385706), descrevi como fazer isso para várias plataforma como uma estrutura e um aplicativo de exemplo do Windows 8 e do Windows Phone 8. Nos dois artigos, falei sobre as decisões que tomei para que o aplicativo pudesse ser testado. Agora, revisitarei essas decisões e mostrarei como testar realmente o aplicativo. Este artigo usa código do Windows 8 e do Windows Phone 8 para os exemplos, mas posso aplicar os conceitos e técnicas a qualquer tipo de aplicativo.

Sobre o aplicativo de exemplo

O aplicativo de exemplo que ilustra como abordo a criação de uma camada da apresentação que pode ser testada é chamado Charmed Reader. O Charmed Reader é um aplicativo de leitura de blog simples que funciona no Windows 8 e no Windows Phone 8. Ele tem a funcionalidade mínima necessária para ilustrar os principais aspectos que desejo abordar. Ele é multiplataforma e funciona quase da mesma maneira nas duas plataformas, com a exceção de que o aplicativo do Windows 8 utiliza alguma funcionalidade específica ao Windows 8. Embora o aplicativo seja básico, há funcionalidade suficiente para o teste de unidade.

O que é o teste de unidade?

A ideia por trás do teste de unidade é usar partes distintas do código (unidades) e escrever métodos de teste que usem o código de maneira esperada e, em seguida, testar para ver se os resultados esperados são obtidos. Esse código de teste é executado usando o mesmo tipo de estrutura do agente de teste. Há várias estruturas de agentes de teste que funcionam com o Visual Studio 2012. No código de exemplo, usei o MSTest, que é interno ao Visual Studio 2012 (e anterior). A meta é fazer com que um único método de teste de unidade tenha como destino um cenário específico. Algumas vezes são necessários vários métodos de teste de unidade para cobrir todos os cenários que você espera que seu método ou propriedade acomode.

Um método de teste de unidade deve seguir um formato consistente para facilitar a compreensão de outros desenvolvedores. O formato a seguir é considerado uma prática recomendada:

  1. Arrange
  2. Act
  3. Assert

Primeiro, talvez exista algum código de configuração que você precise escrever para criar uma instância da classe sob teste bem como quaisquer dependências existentes. Esta é a seção Arrange do teste de unidade.

Depois que o teste de unidade conclui a configuração do cenário para o teste real, você pode executar o método ou a propriedade em questão. Esta é a seção Act do teste de unidade. Você pode executar o método ou a propriedade em questão com parâmetros (quando aplicável) configurados durante a seção Arrange.

Finalmente, quando você tiver executado o método ou a propriedade em questão, o teste precisa verificar se o método ou a propriedade fez exatamente o que era suposto fazer. Esta é a seção Assert do teste de unidade. Durante a fase assert, os métodos de confirmação são chamados para comparar os resultados reais com os resultados esperados. Se os resultados reais forem os esperados, o teste de unidade é aprovado. Caso contrário, o teste é reprovado.

Seguindo esse formato de prática recomendada, meus testes normalmente são parecidos com o seguinte:

[TestMethod]
public void SomeTestMethod()
{
  // Arrange
  // *Insert code to set up test
  // Act
  // *Insert code to call the method or property under test
  // Assert
  // *Insert code to verify the test completed as expected
}

Algumas pessoas usam esse formato sem incluir comentários para chamar as diferentes seções do teste (Arrange/Act/Assert). Prefiro ter comentários separando as três seções para ter certeza de que não perco o controle sobre o que um teste está realmente atuando ou de quando estou apenas configurando.

Um benefício adicional de um conjunto abrangente de testes de unidade bem-escritos é que eles atuam como uma documentação ao vivo do aplicativo. Os novos desenvolvedores que exibirem seu código poderão ver como você esperava que o código fosse usado examinando os diferentes cenários que os testes de unidade cobrem.

Planejando a possibilidade de teste

Se você desejar escrever um aplicativo que possa ser testado, será realmente útil planejar com antecedência. Você desejará criar a arquitetura de seu aplicativo para que seja propícia para o teste de unidade. Os métodos estáticos, as classes sealed, o acesso ao banco de dados e as chamadas de serviços Web, tudo pode dificultar ou impossibilitar o teste de unidade de seu aplicativo. No entanto, com algum planejamento, você pode minimizar o impacto que eles têm sobre seu aplicativo.

O objetivo do aplicativo Charmed Reader é ler postagens de blogs. O download dessas postagens de blog envolve o acesso via Web aos RSS feeds e pode ser muito difícil fazer o teste de unidade dessa funcionalidade. Primeiro, você deve poder executar testes de unidade rapidamente e em um estado desconectado. Contar com o acesso à Web em um teste de unidade potencialmente viola esses princípios.

Além disso, um teste de unidade deve ser repetível. Como normalmente os blogs são atualizados regulamente, pode ser impossível obter o download dos mesmos dados com o decorrer do tempo. Eu já sabia que a execução de testes de unidade da funcionalidade que carrega postagens de blog seria impossível se eu não planejasse antecipadamente.

Isto é o que eu sabia que aconteceria:

  1. O MainViewModel precisava carregar todas as postagens de blog que o usuário desejasse ler de uma vez.
  2. Essas postagens precisavam ser baixadas nos vários RSS feeds que o usuário salvou.
  3. Depois de baixadas, as postagens de blog precisavam ser analisadas em DTOs (objetos de transferência de dados) e disponibilizadas para exibição.

Se eu colocasse o código para baixar os RSS feeds no MainViewModel, ele seria subitamente responsável por mais do que carregar os dados e deixar a exibição executar a vinculação de dados para exibição. O MainViewModel seria responsável por fazer as solicitações Web e analisar os dados XML. O que eu realmente desejo é fazer com que o MainViewModel chame um auxiliar para fazer a solicitação Web e analisar os dados XML. O MainViewModel deve então receber instâncias de objetos que representam as postagens de blog a serem exibidas. Esses elementos são chamados DTOs.

Sabendo disso, posso abstrair o RSS feed carregando e analisando em um objeto auxiliar que o MainViewModel pode chamar. No entanto, isso não é o fim da história. Se eu apenas criar uma classe helper que faça os dados do RSS feed funcionarem, qualquer teste de unidade que eu escrever para MainViewModel em torno dessa funcionalidade também acabaria chamando essa classe helper para fazer o acesso à Web. Como mencionei anteriormente, isso vai contra o objetivo do teste de unidade. Portanto, preciso levar isso um passo adiante.

Se eu criar uma interface para a funcionalidade de carregamento dos dados do RSS feed, poderei fazer com que meu modelo de exibição funcione com a interface em vez de uma classe concreta. Então, poderei fornecer diferentes implementações da interface para quando estiver executando testes de unidade em vez de executar o aplicativo. Esse é o conceito por trás da simulação. Quando eu executar o aplicativo para valer, desejo o objeto real que carrega os dados do RSS feed reais. Quando executo os testes de unidade, desejo um objeto fictício que apenas pretenda carregar os dados de RSS, mas que na verdade nunca acesse a Web. O objeto fictício pode criar dados consistentes que sejam repetíveis e nunca mudem. Assim, meus testes de unidade podem saber exatamente o que esperar sempre.

Com isso em mente, minha interface para carregar as postagens de blog é semelhante a esta:

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

Há apenas um método, GetFeedsAsync, que o MainViewModel pode usar para carregar os dados de postagens de blog. O MainViewModel não precisa se preocupar com como o IRssFeedService carrega ou analisa os dados. O que é importante para o MainViewModel é que a chamada de GetFeedsAsync retornará os dados de postagens de blog assincronamente. Isso é especialmente importante devido à natureza entre plataformas do aplicativo.

O Windows 8 e o Windows Phone 8 têm maneiras diferentes de baixar e analisar dados de RSS feed. Criando a interface IRssFeedService e fazendo com que o MainViewModel interaja com ela, em vez de baixar diretamente os feeds de blogs, evito forçar o MainViewModel a ter várias implementações da mesma funcionalidade.

Usando injeção de dependência, posso garantir que forneço ao MainViewModel a instância correta de IRssFeedService na hora certa. Como mencionei, fornecerei uma instância fictícia de IRssFeedService durante os testes de unidade. Uma coisa interessante sobre o uso de código do Windows 8 e do Windows Phone 8 como a base de uma discussão de teste de unidade é que não há nenhuma estrutura fictícia dinâmica real disponível atualmente para essas plataformas. Como a simulação é uma grande parte de como executo testes de unidade em meu código, precisei inventar minha própria maneira de criar elementos fictícios. O RssFeedServiceMock resultante é mostrado na Figura 1.

Figura 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);
    }
  }
}

Basicamente, desejo fornecer um delegado que possa configurar como os dados são carregados. Se não estiver desenvolvendo para Windows 8 ou Windows Phone 8, há uma boa chance de que você possa usar uma estrutura de simulação dinâmica, como Moq, Rhino Mocks ou NSubstitute. Quer você crie seus próprios elementos fictícios ou use uma estrutura de simulação dinâmica, os mesmos princípios devem ser aplicados.

Agora que tenho a interface IRssFeedService criada e injetada no MainViewModel, o MainViewModel chamando GetFeedsAsync na interface IRssFeedService e o RssFeed­ServiceMock criado e pronto para uso, está na hora de executar o teste de unidade da interação do MainViewModel com o IRssFeedService. Os aspectos importantes que desejo testar nessa interação são se o MainViewModel chama o GetFeedsAsync corretamente e se os dados de feed retornados são os mesmos que o MainViewModel disponibiliza por meio da propriedade FeedData. O teste de unidade da Figura 2 verifica isso para mim.

Figura 2 Tentando a funcionalidade do carregamento de feeds

[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);
}

Sempre que estou executando um teste de unidade de um modelo de exibição (ou de qualquer outro objeto para esse fim), gosto de ter um método auxiliar que me dê a instância real do modelo de exibição a ser testado. Os modelos de exibição têm probabilidade de serem alterados com o tempo, o que pode envolver a injeção de coisas diferentes no modelo de exibição, o que significa parâmetros de construtor diferentes. Se eu criar uma nova instância do modelo de exibição em todos os meus testes de unidade e alterar a assinatura do construtor, precisarei alterar vários testes de unidade junto com ela. No entanto, se eu criar um método auxiliar para criar essa nova instância do modelo de exibição, precisarei fazer a alteração em único local. Nesse caso, GetViewModel é o método auxiliar:

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

Também uso o atributo TestInitialize para garantir que as dependências de MainViewModel sejam criadas novamente antes de executar cada teste. Este é o método TestInitialize que faz isso acontecer:

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

Com isso, cada teste de unidade nessa classe de teste terá instâncias totalmente novas de todos os elementos fictícios quando for executado.

Voltando ao próprio teste, o seguinte código cria meus dados de feed esperados e configura o serviço de feed de RSS fictício para retorná-lo:

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

Observe que não estou adicionando nenhuma instância real de FeedData à lista de expectedFeedData porque não preciso. Preciso apenas garantir que o MainViewModel acabe ficando com a própria lista. Não me preocupo com o que acontece quando a lista realmente contém instâncias de FeedData, pelo menos para este teste.

A parte Action do teste contém a seguinte linha:

var actualFeedData = viewModel.FeedData;

Portanto, posso afirmar que actualFeedData é a mesma instância de expectedFeedData. Se não forem a mesma instância, o MainViewModel não fez seu trabalho e haverá falha no teste de unidade.

Assert.AreSame(expectedFeedData, actualFeedData);

Outra parte importante do aplicativo de exemplo que desejo testar é a navegação. O aplicativo de exemplo Charmed Reader usa navegação baseada em modelo porque desejo manter as exibições e os modelos de exibição separados. O Charmed Reader é um aplicativo entre plataformas e os modelos de exibição são usados nas duas plataformas, embora as exibições precisem ser diferentes para o Windows 8 e o Windows Phone 8. Há várias razões, mas tudo se resume no fato de que cada plataforma tem XAML levemente diferente. Por causa disso, não quis que meus modelos de exibição soubessem de minhas exibições, confundindo as coisas.

A abstração da funcionalidade de navegação por trás de uma interface foi a solução por vários motivos. Em primeiro lugar, cada plataforma tem classes diferentes envolvidas na navegação, e eu não desejava que meu modelo de exibição precisasse se preocupar com essas diferenças. Além disso, nos dois casos, as classes envolvidas na navegação não podem ser simuladas. Portanto, abstrai esses problemas para fora do modelo de exibição e criei a interface INavigator:

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

Injeto o INavigator no MainViewModel por meio do construtor, e o MainViewModel usa o INavigator em um método chamado ViewFeed:

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

Quando examino como o ViewFeed interage com o INavigator, vejo duas coisas que quero verificar à medida que crio o teste de unidade:

  1. O FeedItem que é passado para o ViewFeed é o mesmo FeedItem passado para NavigateToViewModel.
  2. O tipo do modelo de exibição passado para o NavigateToViewModel é FeedItemViewModel.

Antes de eu realmente criar o teste, preciso criar outro elemento fictício, desta vez para INavigator. A Figura 3 mostra o elemento fictício para INavigator. Segui o mesmo padrão de antes com delegados para cada método como uma maneira de executar o código de teste quando o método real for chamado. Mais uma vez, se você estiver trabalhando em uma plataforma com suporte a uma estrutura fictícia, você não precisará criar seu próprio elemento fictício.

Figura 3 Elemento fictício para 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
}

Com minha classe Navigator fictícia estabelecida, posso colocá-la para uso em um teste de unidade, conforme mostrado na Figura 4.

Figura 4 Testando a navegação com o navegador fictício

[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");
}

O que é realmente importante para esse teste é que o FeedItem passado esteja correto e que o modelo de exibição navegado esteja correto. Ao trabalhar com elementos fictícios, é importante lembrar-se do que é importante para você em um determinado teste e com o que você não está preocupado. Para este teste, como tenho a interface INavigator na qual o MainViewModel está funcionando, não preciso me preocupar se a navegação realmente ocorre. Isso é manipulado por qualquer coisa que implemente o INavigator para a instância de tempo de execução. Preciso apenas me preocupar com que o INavigator receba os parâmetros corretos quando a navegação ocorrer.

Blocos secundários que podem ser testados

A área final para a qual vou olhar no teste são os blocos secundários. Blocos secundários estão disponíveis no Windows 8 e no Windows Phone 8, e permitem que os usuários fixem elementos de um aplicativo em suas telas iniciais criando um link profundo em uma parte específica do aplicativo. No entanto, os blocos secundários são manipulados de maneiras totalmente diferentes nas duas plataformas, o que significa que preciso fornecer implementações específicas à plataforma. Apesar da diferença, posso fornecer uma interface consistente para os blocos secundários que posso usar nas duas plataformas:

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

A classe TileInfo é um DTO com propriedades para as duas plataformas combinadas para criar um bloco secundário. Como cada plataforma usa uma combinação de propriedades diferente de TileInfo, cada plataforma precisa ser testada de maneira diferente. Vou verificar especificamente na versão do Windows 8. A Figura 5 mostra como meu modelo de exibição usa ISecondaryPinner.

Na verdade, há duas coisas acontecendo no método Pin na Figura 5. A primeira é a fixação real do bloco secundário. A segunda é salvar o FeedItem em armazenamento local. Portanto, são duas coisas que preciso testar. Como esse método altera a propriedade IsFeedItemPinned no modelo de exibição com base nos resultados da tentativa de fixar o FeedItem, também preciso testar os dois possíveis resultados do método Pin no ISecondaryPinner: true e false. A Figure 6 mostra o primeiro teste que implementei, que testa o cenário bem-sucedido.

Figura 5 Usando ISecondaryPinner

public async Task Pin(Windows.UI.Xaml.FrameworkElement anchorElement)
{
  // Pin the feed item, then save it locally to make sure it's still available
  // when they return.
  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();
  }
}

Figura 6 Testando um Pin bem-sucedido

[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("https://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");
}

Há um pouco mais de configuração envolvida nisso do que em testes anteriores. Primeiro, depois do controlador, configurei uma instância de FeedItem. Observe que eu chamo ToString em Guids para Title e para Author. Como não me importo com valores reais, só me interessa que eles tenham valores que eu possa comparar na seção assert. Como Link é uma Uri, não preciso de uma Uri válida para que isso funcione, portanto, forneci uma. Novamente, não importa o que é a Uri real, apenas que ela seja válida. O restante da configuração envolve garantir que eu capture as interações para fixação e salvamento para comparação na seção assert. A chave para garantir que esse código realmente testará o cenário bem-sucedido é que PinDelegate retorne true, indicando êxito.

A Figura 7 mostra quase o mesmo teste, mas para o cenário malsucedido. O fato de que PinDelegate retorna false é o que garante que o teste está focalizando o cenário malsucedido. No cenário malsucedido, também preciso verificar na seção assert se SaveAsync não foi chamado.

Figura 7 Testando um Pin malsucedido

[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("https://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");
}

Criar aplicativos que podem ser testados é um desafio. É especialmente desafiante testar a camada de apresentação onde a interação do usuário está envolvida. Saber com antecedência que você criará um aplicativo que possa ser testado permite tomar decisões em cada etapa para favorecer a capacidade de teste. Você também pode procurar itens que tornarão seu aplicativo menos capaz de ser testado e inventar maneiras de corrigi-los.

Em três artigos, abordei a criação de aplicativos que podem ser testados com o padrão MVVM, especificamente para Windows 8 e Windows Phone 8. No primeiro artigo, abordei a criação de aplicativos do Windows 8 que possam ser testados, utilizando recursos específicos do Windows 8 que não são fáceis de testar. O segundo artigo expandiu a discussão para incluir o desenvolvimento de aplicativos entre plataformas que podem ser testados com o Windows 8 e o Windows Phone 8. Com este artigo, mostrei como abordei o teste de aplicativos que foram difíceis de fazer com que pudessem ser testados.

O MVVM é um tópico amplo, com várias interpretações diferentes. É um prazer poder compartilhar minha interpretação de um tópico tão interessante. Considero o uso do MVVM muito valioso, especialmente no que diz respeito à capacidade de teste. Explorar a capacidade de teste também é muito estimulante e útil e fico feliz por compartilhar minha abordagem de escrever um aplicativo que pode ser testado.

Brent Edwards é consultor líder principal da Magenic, uma empresa de desenvolvimento de aplicativos personalizados com foco no pacote de programas Microsoft e no desenvolvimento de aplicativos para celulares. Ele também é cofundador do Twin Cities Windows 8 User Group em Minneapolis, Minnesota. Para entrar em contato, escreva para brente@magenic.com.

AGRADECEMOS ao seguinte especialista técnico pela revisão deste artigo: Jason Bock (Magenic)
Jason Bock é líder de práticas na Magenic (www.magenic.com). Também é coautor do livro Metaprogramming in .NET (www.manning.com/hazzard). Entre em contato pelo endereço jasonbock.net ou no twitter: @jasonbock.