Aplicativos empresariais de teste de unidade
Observação
Este e-book foi publicado na primavera de 2017 e não foi atualizado desde então. Há muito no livro que permanece valioso, mas parte do material está desatualizado.
Os aplicativos móveis têm problemas exclusivos com os quais os aplicativos de desktop e baseados na Web não precisam se preocupar. Os usuários móveis diferem pelos dispositivos que usam, pela conectividade de rede, pela disponibilidade de serviços e por uma série de outros fatores. Portanto, os aplicativos móveis devem ser testados, pois serão usados no mundo real para melhorar sua qualidade, confiabilidade e desempenho. Há muitos tipos de testes que devem ser executados em um aplicativo, incluindo testes de unidade, testes de integração e testes de interface do usuário, sendo o teste de unidade a forma mais comum de teste.
Um teste de unidade usa uma pequena unidade do aplicativo, normalmente um método, a isola do restante do código e verifica se ela se comporta conforme o esperado. Seu objetivo é verificar se cada unidade de funcionalidade funciona conforme o esperado, para que os erros não se propaguem por todo o aplicativo. Detectar um bug no local em que ele ocorre é mais eficiente que observar o efeito de um bug indiretamente em um ponto secundário de falha.
O teste de unidade tem o maior efeito na qualidade do código quando é parte integrante do fluxo de trabalho de desenvolvimento de software. Assim que um método for escrito, testes de unidade devem ser escritos para verificar o comportamento do método em resposta a casos padrão, de limite e incorretos de dados de entrada e que verifiquem quaisquer suposições explícitas ou implícitas feitas pelo código. Como alternativa, com o desenvolvimento orientado a testes, os testes unitários são escritos antes do código. Nesse cenário, os testes de unidade atuam como documentação de design e especificações funcionais.
Observação
Os testes de unidade são muito eficazes contra a regressão – ou seja, funcionalidade que costumava funcionar, mas foi perturbada por uma atualização defeituosa.
Os testes de unidade normalmente usam o padrão arrange-act-assert:
- A seção arrange do método de teste de unidade inicializa objetos e define o valor dos dados que são passados para o método em teste.
- A seção act invoca o método em teste com os argumentos necessários.
- A seção assert verifica se a ação do método em teste se comporta conforme o esperado.
Seguir esse padrão garante que os testes de unidade sejam legíveis e consistentes.
Injeção de dependência e teste de unidade
Uma das motivações para adotar uma arquitetura flexível é que ela facilita o teste de unidade. Um dos tipos registrados no Autofac é a OrderService
classe. O código a seguir mostra um exemplo disso:
public class OrderDetailViewModel : ViewModelBase
{
private IOrderService _ordersService;
public OrderDetailViewModel(IOrderService ordersService)
{
_ordersService = ordersService;
}
...
}
A OrderDetailViewModel
classe tem uma dependência do IOrderService
tipo que o contêiner resolve quando instancia um OrderDetailViewModel
objeto. No entanto, em vez de criar um OrderService
objeto para testar a unidade da OrderDetailViewModel
classe, substitua o OrderService
objeto por uma simulação para fins dos testes. A Figura 10-1 ilustra essa relação.
Figura 10-1: Classes que implementam a interface IOrderService
Essa abordagem permite que o OrderService
objeto seja passado para a OrderDetailViewModel
classe em tempo de execução e, no interesse da capacidade de teste, permite que a OrderMockService
classe seja passada para a OrderDetailViewModel
classe no momento do teste. A principal vantagem dessa abordagem é que ela permite que testes de unidade sejam executados sem exigir recursos pesados, como serviços da Web ou bancos de dados.
Testando aplicativos MVVM
Modelos de teste e modelos de exibição de aplicativos MVVM são idênticos a testar qualquer outra classe, e as mesmas ferramentas e técnicas – como teste de unidade e simulação, podem ser usadas. No entanto, há alguns padrões que são típicos para modelar e exibir classes de modelo, que podem se beneficiar de técnicas específicas de teste de unidade.
Dica
Teste uma coisa com cada teste de unidade. Não busque fazer um exercício de teste de unidade em mais de um aspecto do comportamento da unidade. Isso leva a testes difíceis de ler e atualizar. Também pode causar confusão ao interpretar uma falha.
O aplicativo móvel eShopOnContainers executa testes de unidade, que suportam dois tipos diferentes de testes de unidade:
- Fatos são testes que são sempre verdadeiros, que testam condições invariantes.
- Teorias são testes que são verdadeiros apenas para um determinado conjunto de dados.
Os testes de unidade incluídos no aplicativo móvel eShopOnContainers são testes de fatos e, portanto, cada método de teste de unidade é decorado com o [Fact]
atributo.
Observação
Os testes xUnit são executados por um executor de teste. Para executar o executor de teste, execute o projeto eShopOnContainers.TestRunner para a plataforma necessária.
Testando a funcionalidade assíncrona
Ao implementar o padrão MVVM, os modelos de exibição geralmente invocam operações em serviços, muitas vezes de forma assíncrona. Os testes de código que invocam essas operações normalmente usam simulações como substituições para os serviços reais. O exemplo de código a seguir demonstra o teste da funcionalidade assíncrona passando um serviço fictício para um modelo de exibição:
[Fact]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
var orderService = new OrderMockService();
var orderViewModel = new OrderDetailViewModel(orderService);
var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
await orderViewModel.InitializeAsync(order);
Assert.NotNull(orderViewModel.Order);
}
Este teste de unidade verifica se a propriedade Order
da instância OrderDetailViewModel
terá um valor depois que o método InitializeAsync
tiver sido invocado. O método InitializeAsync
é invocado quando a exibição correspondente do modelo de exibição é navegada. Para obter mais informações sobre navegação, confira Navegação.
Quando a instância OrderDetailViewModel
é criada, espera-se que uma instância OrderService
seja especificada como um argumento. No entanto, OrderService
recupera dados de um serviço Web. Portanto, uma OrderMockService
instância, que é uma versão fictícia OrderService
da classe, é especificada como o argumento para o OrderDetailViewModel
construtor. Em seguida, quando o método do modelo de exibição é invocado, o que invoca operações, os dados fictícios IOrderService
são recuperados InitializeAsync
em vez de se comunicar com um serviço Web.
Testando implementações INotifyPropertyChanged
A implementação da interface INotifyPropertyChanged
permite que as exibições reajam às alterações originadas de modelos e modelos de exibição. Essas alterações não se limitam aos dados mostrados nos controles – elas também são usadas para controlar a exibição, como estados do modelo de exibição que fazem com que as animações sejam iniciadas ou os controles sejam desabilitados.
As propriedades que podem ser atualizadas diretamente pelo teste de unidade podem ser testadas anexando um manipulador de eventos ao evento PropertyChanged
e verificando se o evento é gerado após a definição de um novo valor para a propriedade. O exemplo de código a seguir mostra tal teste:
[Fact]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
bool invoked = false;
var orderService = new OrderMockService();
var orderViewModel = new OrderDetailViewModel(orderService);
orderViewModel.PropertyChanged += (sender, e) =>
{
if (e.PropertyName.Equals("Order"))
invoked = true;
};
var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
await orderViewModel.InitializeAsync(order);
Assert.True(invoked);
}
Esse teste de unidade invoca o método InitializeAsync
da classe OrderViewModel
, que faz com que sua propriedade Order
seja atualizada. O teste de unidade será aprovado, desde que o evento PropertyChanged
seja gerado para a propriedade Order
.
Testando a comunicação baseada em mensagens
Os modelos de exibição que usam a classe MessagingCenter
para se comunicar entre classes flexíveis podem ser testados por unidade assinando a mensagem que está sendo enviada pelo código em teste, conforme demonstrado no exemplo de código a seguir:
[Fact]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
bool messageReceived = false;
var catalogService = new CatalogMockService();
var catalogViewModel = new CatalogViewModel(catalogService);
Xamarin.Forms.MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
this, MessageKeys.AddProduct, (sender, arg) =>
{
messageReceived = true;
});
catalogViewModel.AddCatalogItemCommand.Execute(null);
Assert.True(messageReceived);
}
Este teste de unidade verifica se o CatalogViewModel
publica a mensagem AddProduct
em resposta à sua execução AddCatalogItemCommand
. Como a classe MessagingCenter
dá suporte a assinaturas de mensagens multicast, o teste de unidade pode assinar a mensagem AddProduct
e executar um delegado de retorno de chamada em resposta ao recebimento. Esse delegado de retorno de chamada, especificado como uma expressão lambda, define um boolean
campo que é usado pela Assert
instrução para verificar o comportamento do teste.
Testando o tratamento de exceções
Testes de unidade também podem ser gravados que verificam se exceções específicas são geradas para ações ou entradas inválidas, conforme demonstrado no exemplo de código a seguir:
[Fact]
public void InvalidEventNameShouldThrowArgumentExceptionText()
{
var behavior = new MockEventToCommandBehavior
{
EventName = "OnItemTapped"
};
var listView = new ListView();
Assert.Throws<ArgumentException>(() => listView.Behaviors.Add(behavior));
}
Esse teste de unidade gerará uma exceção, pois o ListView
controle não tem um evento chamado OnItemTapped
. O método Assert.Throws<T>
é um método genérico em que T
é o tipo da exceção esperada. O argumento passado para o método Assert.Throws<T>
é uma expressão lambda que gerará a exceção. Portanto, o teste de unidade será aprovado desde que a expressão lambda gere um ArgumentException
.
Dica
Evite gravar testes de unidade que examinam cadeias de caracteres de mensagem de exceção. As cadeias de caracteres de mensagem de exceção podem mudar ao longo do tempo e, portanto, os testes de unidade que dependem de sua presença são considerados frágeis.
Validação de teste
Há dois aspectos para testar a implementação de validação: testar se todas as regras de validação foram implementadas corretamente e testar se a classe é executada ValidatableObject<T>
conforme o esperado.
A lógica de validação geralmente é simples de testar, pois normalmente é um processo autocontido em que a saída depende da entrada. Deve haver testes nos resultados da invocação do método Validate
em cada propriedade que tenha pelo menos uma regra de validação associada, conforme demonstrado no exemplo de código a seguir:
[Fact]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
var mockViewModel = new MockViewModel();
mockViewModel.Forename.Value = "John";
mockViewModel.Surname.Value = "Smith";
bool isValid = mockViewModel.Validate();
Assert.True(isValid);
}
Este teste de unidade verifica se a validação é bem-sucedida quando as duas propriedades ValidatableObject<T>
na instância MockViewModel
têm dados.
Além de verificar se a validação é bem-sucedida, os testes de unidade de validação também devem verificar os valores da propriedade Value
, IsValid
e Errors
de cada instância ValidatableObject<T>
, para verificar se a classe é executada conforme o esperado. O exemplo de código a seguir demonstra um teste de unidade que faz isso:
[Fact]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
var mockViewModel = new MockViewModel();
mockViewModel.Forename.Value = "John";
bool isValid = mockViewModel.Validate();
Assert.False(isValid);
Assert.NotNull(mockViewModel.Forename.Value);
Assert.Null(mockViewModel.Surname.Value);
Assert.True(mockViewModel.Forename.IsValid);
Assert.False(mockViewModel.Surname.IsValid);
Assert.Empty(mockViewModel.Forename.Errors);
Assert.NotEmpty(mockViewModel.Surname.Errors);
}
Este teste de unidade verifica se a validação falha quando a propriedade Surname
do MockViewModel
não tem dados e as propriedades Value
, IsValid
e Errors
de cada instância ValidatableObject<T>
são definidas corretamente.
Resumo
Um teste de unidade usa uma pequena unidade do aplicativo, normalmente um método, a isola do restante do código e verifica se ela se comporta conforme o esperado. Seu objetivo é verificar se cada unidade de funcionalidade funciona conforme o esperado, para que os erros não se propaguem por todo o aplicativo.
O comportamento de um objeto em teste pode ser isolado substituindo objetos dependentes por objetos fictícios que simulam o comportamento dos objetos dependentes. Isso permite que os testes de unidade sejam executados sem a necessidade de recursos pesados, como serviços Web ou bancos de dados.
Testar modelos e exibir modelos de aplicativos MVVM é idêntico ao teste de outras classes, e as mesmas ferramentas e técnicas podem ser usadas.