Teste de unidade
Dica
Esse conteúdo é um trecho do livro eletrônico, Padrões de Aplicativo Empresarial Usando .NETMAUI, disponível em .NET Docs ou em PDF para download gratuito que pode ser lido off-line.
Os aplicativos multiplataforma apresentam problemas semelhantes aos aplicativos de desktop e baseados na Web. Os usuários móveis serão diferentes por seus dispositivos, conectividade de rede, disponibilidade de serviços e vários outros fatores. Portanto, os aplicativos multiplataforma devem ser testados, pois seriam usados no mundo real para melhorar sua qualidade, confiabilidade e desempenho. Muitos tipos de teste devem ser executados em um aplicativo, incluindo teste de unidade, teste de integração e teste de interface do usuário. O teste de unidade é a forma mais comum e essencial para criar aplicativos de alta qualidade.
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. Sua meta é verificar se cada unidade de funcionalidade é executada conforme o esperado, para que os erros não se propaguem por todo o aplicativo. Detectar um bug onde ele ocorre é mais eficiente do que observar o efeito de um bug indiretamente em um ponto de falha secundário.
O teste de unidade tem o efeito mais significativo na qualidade do código quando é parte integrante do fluxo de trabalho de desenvolvimento de software. Os testes de unidade podem atuar como documentação de design e especificações funcionais para um aplicativo. Assim que um método tiver sido escrito, devem ser gravados testes de unidade que verifiquem o comportamento do método em resposta a casos de dados de entrada padrão, limite e incorretos e verifiquem qualquer suposição explícita ou implícita feita pelo código. Como alternativa, com o desenvolvimento controlado por teste, os testes de unidade são gravados antes do código. Para obter mais informações sobre o desenvolvimento controlado por teste e como implementá-lo, consulte Passo a passo: desenvolvimento controlado por teste usando o Gerenciador de Testes..
Observação
Os testes de unidade são muito eficazes em relação à regressão. Ou seja, funcionalidade que costumava funcionar, mas sofreu interferência por uma atualização com falha.
Os testes de unidade normalmente usam o padrão arrange-act-assert:
Etapa | Descrição |
---|---|
Organizar | Inicializa objetos e define o valor dos dados que são passados para o método em teste. |
Agir | Invoca o método em teste com os argumentos necessários. |
Assert | Verifica se a ação do método em teste se comporta conforme o esperado. |
Esse padrão garante que os testes de unidade sejam legíveis, autodescritivos 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 com o serviço de injeção de dependência é a interface IAppEnvironmentService
. O código a seguir mostra um exemplo disso:
public class OrderDetailViewModel : ViewModelBase
{
private IAppEnvironmentService _appEnvironmentService;
public OrderDetailViewModel(
IAppEnvironmentService appEnvironmentService,
IDialogService dialogService, INavigationService navigationService, ISettingsService settingsService)
: base(dialogService, navigationService, settingsService)
{
_appEnvironmentService = appEnvironmentService;
}
}
A classe OrderDetailViewModel
tem uma dependência do tipo IAppEnvironmentService
, que o contêiner de injeção de dependência resolve quando cria uma instância de um objeto OrderDetailViewModel
. No entanto, em vez de criar um objeto IAppEnvironmentService
que utiliza servidores reais, dispositivos e configurações para testar a unidade da classe OrderDetailViewModel
, substitua o objeto IAppEnvironmentService
por um objeto fictício para fins dos testes. Um objeto fictício é aquele que tem a mesma assinatura de um objeto ou uma interface, mas é criado de maneira específica para ajudar no teste de unidade. Geralmente, ele é usado com injeção de dependência para fornecer implementações específicas de interfaces para testar diferentes cenários de fluxo de trabalho e dados.
Essa abordagem permite que o objeto IAppEnvironmentService
seja passado para a classe OrderDetailViewModel
em runtime e, no interesse da capacidade de teste, permite que uma classe fictícia seja passada para a classe OrderDetailViewModel
em tempo de teste. A principal vantagem dessa abordagem é que ela permite que os testes de unidade sejam executados sem exigir recursos complicados, como recursos de plataforma de runtime, serviços Web ou bancos de dados.
Testando aplicativos MVVM
Testar modelos e exibir modelos de aplicativos MVVM é idêntico ao teste de qualquer outra classe e usa as mesmas ferramentas e técnicas; isso inclui recursos como teste de unidade e simulação. No entanto, alguns padrões típicos para modelar e exibir classes de modelo podem se beneficiar de técnicas específicas de teste de unidade.
Dica
Teste uma coisa com cada teste de unidade. À medida que a complexidade de um teste se expande, torna a verificação desse teste mais difícil. Limitando um teste de unidade a uma única preocupação, podemos garantir que nossos testes sejam mais repetíveis, isolados e tenham um tempo de execução menor. Confira Práticas recomendadas de teste de unidade com o .NET para ver mais práticas recomendadas.
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 multiplataforma eShop usa o MSTest para executar testes de unidade, que dão suporte a dois tipos diferentes de testes de unidade:
Tipo de teste | Atributo | Descrição |
---|---|---|
TestMethod | TestMethod |
Define o método de teste real a ser executado. |
DataSource | DataSource |
Testes que só são verdadeiros para um determinado conjunto de dados. |
Os testes de unidade incluídos com o aplicativo multiplataforma eShop são TestMethod; portanto, cada método de teste de unidade é decorado com o atributo TestMethod
. Além do MSTest, há várias outras estruturas de teste disponíveis, incluindo NUnit e xUnit.
Testar 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:
[TestMethod]
public async Task OrderPropertyIsNotNullAfterViewModelInitializationTest()
{
// Arrange
var orderService = new OrderMockService();
var orderViewModel = new OrderDetailViewModel(orderService);
// Act
var order = await orderService.GetOrderAsync(1, GlobalSetting.Instance.AuthToken);
await orderViewModel.InitializeAsync(order);
// Assert
Assert.IsNotNull(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 IOrderService
seja especificada como um argumento. No entanto, OrderService
recupera dados de um serviço Web. Portanto, uma instância OrderMockService
, uma versão simulada da classe OrderService
, é especificada como o argumento para o construtor OrderDetailViewModel
. Em seguida, os dados fictícios são recuperados em vez de se comunicar com um serviço Web quando o método InitializeAsync
do modelo de exibição é invocado, que usa as operações IOrderService
.
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 de 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:
[TestMethod]
public async Task SettingOrderPropertyShouldRaisePropertyChanged()
{
var 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.IsTrue(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 mensagem
Os modelos de exibição que usam a classe MessagingCenter
para se comunicar entre classes flexivelmente acopladas 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:
[TestMethod]
public void AddCatalogItemCommandSendsAddProductMessageTest()
{
var messageReceived = false;
var catalogService = new CatalogMockService();
var catalogViewModel = new CatalogViewModel(catalogService);
MessagingCenter.Subscribe<CatalogViewModel, CatalogItem>(
this, MessageKeys.AddProduct, (sender, arg) =>
{
messageReceived = true;
});
catalogViewModel.AddCatalogItemCommand.Execute(null);
Assert.IsTrue(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 campo booliano usado pela instrução Assert
para verificar o comportamento do teste.
Tratamento de exceções C++
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:
[TestMethod]
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 porque o controle ListView
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.
Testando a validação
Há dois aspectos para testar a implementação de validação: testar se todas as regras de validação são implementadas corretamente e testar se a classe ValidatableObject<T>
executa 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:
[TestMethod]
public void CheckValidationPassesWhenBothPropertiesHaveDataTest()
{
var mockViewModel = new MockViewModel();
mockViewModel.Forename.Value = "John";
mockViewModel.Surname.Value = "Smith";
var isValid = mockViewModel.Validate();
Assert.IsTrue(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:
[TestMethod]
public void CheckValidationFailsWhenOnlyForenameHasDataTest()
{
var mockViewModel = new MockViewModel();
mockViewModel.Forename.Value = "John";
bool isValid = mockViewModel.Validate();
Assert.IsFalse(isValid);
Assert.IsNotNull(mockViewModel.Forename.Value);
Assert.IsNull(mockViewModel.Surname.Value);
Assert.IsTrue(mockViewModel.Forename.IsValid);
Assert.IsFalse(mockViewModel.Surname.IsValid);
Assert.AreEqual(mockViewModel.Forename.Errors.Count(), 0);
Assert.AreNotEqual(mockViewModel.Surname.Errors.Count(), 0);
}
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. Sua meta é verificar se cada unidade de funcionalidade é executada 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 exigir recursos complicados, como recursos de plataforma de runtime, 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.