Compartilhar via


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.

Miniatura da capa do livro eletrônico MAUIPadrões de aplicativo empresarial usando .NET.

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.