Testes de unidade do Razor Pages no ASP.NET Core

O ASP.NET Core dá suporte a testes de unidade dos aplicativos Razor Pages. Os testes da DAL (camada de acesso a dados) e dos modelos de página ajudam a garantir:

  • As partes de um aplicativo Razor Pages funcionam de forma independente e em conjunto como uma unidade durante a construção do aplicativo.
  • Classes e métodos têm escopos limitados de responsabilidade.
  • Existe uma documentação adicional sobre como o aplicativo deve se comportar.
  • As regressões, que são erros causados por atualizações no código, são encontradas durante a criação e a implantação automatizadas.

Este tópico pressupõe que você tenha uma compreensão básica dos aplicativos Razor Pages e testes de unidade. Se você não estiver familiarizado com os aplicativos Razor Pages ou conceitos de teste, confira os seguintes tópicos:

Exibir ou baixar código de exemplo (como baixar)

O projeto de exemplo é composto por dois aplicativos:

Aplicativo Pasta do projeto Descrição
Aplicativo de mensagens src/RazorPagesTestSample Permite que um usuário adicione uma mensagem, exclua uma mensagem, exclua todas as mensagens e analise mensagens (localize o número médio de palavras por mensagem).
Aplicativo de teste tests/RazorPagesTestSample.Tests Usado para testar a unidade do modelo de página DAL e o Índice do aplicativo de mensagens.

Os testes podem ser executados usando os recursos de teste nativos do IDE, como o Visual Studio. Se estiver usando o Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando na pasta tests/RazorPagesTestSample.Tests:

dotnet test

Organização do aplicativo de mensagens

O aplicativo de mensagens é um sistema de mensagens do Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (localize o número médio de palavras por mensagem).
  • Uma mensagem é descrita pela Message classe (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é necessária e limitada a 200 caracteres.
  • As mensagens são armazenadas usando o banco de dados na memória do Entity Framework†.
  • O aplicativo contém uma DAL em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs). Os métodos DAL são marcados como virtual, o que permite simular os métodos para uso nos testes.
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o repositório de mensagens será inicializado com três mensagens. Essas mensagens propagadas também são usadas em testes.

†O tópico do EF, Teste com InMemory, explica como usar um banco de dados na memória para testes com MSTest. Este tópico usa a estrutura de teste xUnit. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo de exemplo não use o padrão de repositório e não seja um exemplo eficaz do padrão UoW (Unidade de Trabalho),o Razor Pages dá suporte a esses padrões de desenvolvimento. Para obter mais informações, confira Criando a camada de persistência de infraestrutura e Lógica do controlador de teste no ASP.NET Core (o exemplo implementa o padrão do repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro da pasta tests/RazorPagesTestSample.Tests .

Testar pasta do aplicativo Descrição
UnitTests
  • DataAccessLayerTest.cs contém os testes de unidade para a DAL.
  • IndexPageTests.cs contém os testes de unidade para o modelo de página Índice.
Utilitários Contém o método TestDbContextOptions usado para criar novas opções de contexto de banco de dados para cada teste de unidade da DAL para que o banco de dados seja redefinido para sua condição de linha de base para cada teste.

A estrutura de teste é xUnit. A estrutura de simulação de objeto é Moq.

Testes de unidade da DAL (camada de acesso a dados)

O aplicativo de mensagens tem uma DAL com quatro métodos contidos na classe AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Cada método tem um ou dois testes de unidade no aplicativo de teste.

Método DAL Função
GetMessagesAsync Obtém uma List<Message> do banco de dados classificado pela propriedade Text.
AddMessageAsync Adiciona uma Message ao banco de dados.
DeleteAllMessagesAsync Exclui todas as entradas de Message do banco de dados.
DeleteMessageAsync Exclui uma única Message do banco de dados Id.

Os testes de unidade da DAL exigem DbContextOptions ao criar um novo AppDbContext para cada teste. Uma abordagem para criar DbContextOptions para cada teste é usar um DbContextOptionsBuilder:

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

O problema com essa abordagem é que cada teste recebe o banco de dados em qualquer estado em que o teste anterior o deixou. Isso pode ser problemático ao tentar escrever testes de unidade atômica que não interferem uns nos outros. Para forçar o AppDbContext a usar um novo contexto de banco de dados para cada teste, forneça uma instância DbContextOptions baseada em um novo provedor de serviços. O aplicativo de teste mostra como fazer isso usando seu método de classe UtilitiesTestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

Usar DbContextOptions nos testes de unidade da DAL permite que cada teste seja executado atomicamente com uma nova instância de banco de dados:

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Cada método de teste na classe DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) segue um padrão Arrange-Act-Assert semelhante:

  1. Arrange: o banco de dados está configurado para o teste e/ou o resultado esperado é definido.
  2. Act: o teste é executado.
  3. Assert: instruções assert são feitas para determinar se o resultado do teste é bem-sucedido.

Por exemplo, o método DeleteMessageAsync é responsável por remover uma única mensagem identificada por seu Id (src/RazorPagesTestSample/Data/AppDbContext.cs):

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Há dois testes para esse método. Um teste verifica se o método exclui uma mensagem quando a mensagem está presente no banco de dados. O outro método testa que o banco de dados não será alterado se a mensagem Id de exclusão não existir. O método DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound completo é mostrado abaixo:

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Primeiro, o método executa a etapa Arrange, em que a preparação para a etapa Act ocorre. As mensagens de propagação são obtidas e mantidas em seedMessages. As mensagens de propagação são salvas no banco de dados. A mensagem com um Id de 1 é definida para exclusão. Quando o método DeleteMessageAsync é executado, as mensagens esperadas devem ter todas as mensagens, exceto aquela com um Id de 1. A variável expectedMessages representa esse resultado esperado.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

O método atua: o método DeleteMessageAsync é executado passando o recId de 1:

// Act
await db.DeleteMessageAsync(recId);

Por fim, o método obtém Messages do contexto e o compara com expectedMessages declarando de que as duas são iguais:

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Para comparar se as duas List<Message> são iguais:

  • As mensagens são ordenadas por Id.
  • Os pares de mensagens são comparados na propriedade Text.

Um método de teste semelhante, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, verifica o resultado da tentativa de excluir uma mensagem que não existe. Nesse caso, as mensagens esperadas no banco de dados devem ser iguais às mensagens reais após a execução do método DeleteMessageAsync. Não deve haver nenhuma alteração no conteúdo do banco de dados:

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        try
        {
            await db.DeleteMessageAsync(recId);
        }
        catch
        {
            // recId doesn't exist
        }

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Testes de unidade dos métodos de modelo de página

Outro conjunto de testes de unidade é responsável por testes de métodos de modelo de página. No aplicativo de mensagens, os modelos de página Índice são encontrados na classe IndexModel em src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Método de modelo de página Função
OnGetAsync Obtém as mensagens da DAL para a interface do usuário usando o método GetMessagesAsync.
OnPostAddMessageAsync Se o ModelState for válido, chamará AddMessageAsync para adicionar uma mensagem ao banco de dados.
OnPostDeleteAllMessagesAsync Chama DeleteAllMessagesAsync para excluir todas as mensagens no banco de dados.
OnPostDeleteMessageAsync Executa DeleteMessageAsync para excluir uma mensagem com o Id especificado.
OnPostAnalyzeMessagesAsync Se uma ou mais mensagens estiverem no banco de dados, calculará o número médio de palavras por mensagem.

Os métodos de modelo de página são testados usando sete testes na classe IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Os testes usam o padrão Arrange-Assert-Act familiar. Esses testes se concentram em:

  • Determinar se os métodos seguem o comportamento correto quando o ModelState é inválido.
  • Confirmar se os métodos produzem o IActionResult correto.
  • Verificar se as atribuições de valor da propriedade são feitas corretamente.

Esse grupo de testes geralmente simula os métodos da DAL para produzir dados esperados para a etapa Act em que um método de modelo de página é executado. Por exemplo, o método GetMessagesAsync do AppDbContext é simulado para produzir saída. Quando um método de modelo de página executa esse método, a simulação retorna o resultado. Os dados não são provenientes do banco de dados. Isso cria condições de teste previsíveis e confiáveis para usar a DAL nos testes de modelo de página.

O teste OnGetAsync_PopulatesThePageModel_WithAListOfMessages mostra como o método GetMessagesAsync é simulado para o modelo de página:

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Quando o método OnGetAsync é executado na etapa Act, ele chama o método GetMessagesAsync do modelo de página.

Etapa Act do teste de unidade (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

Método OnGetAsync do modelo de página IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

O método GetMessagesAsync na DAL não retorna o resultado dessa chamada de método. A versão simulada do método retorna o resultado.

Na etapa Assert, as mensagens reais (actualMessages) são atribuídas na propriedade Messages do modelo de página. Uma verificação de tipo também é executada quando as mensagens são atribuídas. As mensagens esperadas e reais são comparadas por suas propriedades Text. O teste afirma que as duas instâncias List<Message> contêm as mesmas mensagens.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Outros testes neste grupo criam objetos de modelo de página que incluem o DefaultHttpContext, o ModelStateDictionary e um ActionContext para estabelecer o PageContext, um ViewDataDictionary e um PageContext. Eles são úteis na realização de testes. Por exemplo, o aplicativo de mensagens estabelece um erro ModelState com AddModelError para marcar que um PageResult válido é retornado quando OnPostAddMessageAsync é executado:

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Recursos adicionais

O ASP.NET Core dá suporte a testes de unidade dos aplicativos Razor Pages. Os testes da DAL (camada de acesso a dados) e dos modelos de página ajudam a garantir:

  • As partes de um aplicativo Razor Pages funcionam de forma independente e em conjunto como uma unidade durante a construção do aplicativo.
  • Classes e métodos têm escopos limitados de responsabilidade.
  • Existe uma documentação adicional sobre como o aplicativo deve se comportar.
  • As regressões, que são erros causados por atualizações no código, são encontradas durante a criação e a implantação automatizadas.

Este tópico pressupõe que você tenha uma compreensão básica dos aplicativos Razor Pages e testes de unidade. Se você não estiver familiarizado com os aplicativos Razor Pages ou conceitos de teste, confira os seguintes tópicos:

Exibir ou baixar código de exemplo (como baixar)

O projeto de exemplo é composto por dois aplicativos:

Aplicativo Pasta do projeto Descrição
Aplicativo de mensagens src/RazorPagesTestSample Permite que um usuário adicione uma mensagem, exclua uma mensagem, exclua todas as mensagens e analise mensagens (localize o número médio de palavras por mensagem).
Aplicativo de teste tests/RazorPagesTestSample.Tests Usado para testar a unidade do modelo de página DAL e o Índice do aplicativo de mensagens.

Os testes podem ser executados usando os recursos de teste nativos do IDE, como o Visual Studio. Se estiver usando o Visual Studio Code ou a linha de comando, execute o seguinte comando em um prompt de comando na pasta tests/RazorPagesTestSample.Tests:

dotnet test

Organização do aplicativo de mensagens

O aplicativo de mensagens é um sistema de mensagens do Razor Pages com as seguintes características:

  • A página Índice do aplicativo (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornece uma interface do usuário e métodos de modelo de página para controlar a adição, exclusão e análise de mensagens (localize o número médio de palavras por mensagem).
  • Uma mensagem é descrita pela Message classe (Data/Message.cs) com duas propriedades: Id (chave) e Text (mensagem). A propriedade Text é necessária e limitada a 200 caracteres.
  • As mensagens são armazenadas usando o banco de dados na memória do Entity Framework†.
  • O aplicativo contém uma DAL em sua classe de contexto de banco de dados, AppDbContext (Data/AppDbContext.cs). Os métodos DAL são marcados como virtual, o que permite simular os métodos para uso nos testes.
  • Se o banco de dados estiver vazio na inicialização do aplicativo, o repositório de mensagens será inicializado com três mensagens. Essas mensagens propagadas também são usadas em testes.

†O tópico do EF, Teste com InMemory, explica como usar um banco de dados na memória para testes com MSTest. Este tópico usa a estrutura de teste xUnit. Os conceitos de teste e as implementações de teste em diferentes estruturas de teste são semelhantes, mas não idênticos.

Embora o aplicativo de exemplo não use o padrão de repositório e não seja um exemplo eficaz do padrão UoW (Unidade de Trabalho),o Razor Pages dá suporte a esses padrões de desenvolvimento. Para obter mais informações, confira Criando a camada de persistência de infraestrutura e Lógica do controlador de teste no ASP.NET Core (o exemplo implementa o padrão do repositório).

Testar a organização do aplicativo

O aplicativo de teste é um aplicativo de console dentro da pasta tests/RazorPagesTestSample.Tests .

Testar pasta do aplicativo Descrição
UnitTests
  • DataAccessLayerTest.cs contém os testes de unidade para a DAL.
  • IndexPageTests.cs contém os testes de unidade para o modelo de página Índice.
Utilitários Contém o método TestDbContextOptions usado para criar novas opções de contexto de banco de dados para cada teste de unidade da DAL para que o banco de dados seja redefinido para sua condição de linha de base para cada teste.

A estrutura de teste é xUnit. A estrutura de simulação de objeto é Moq.

Testes de unidade da DAL (camada de acesso a dados)

O aplicativo de mensagens tem uma DAL com quatro métodos contidos na classe AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Cada método tem um ou dois testes de unidade no aplicativo de teste.

Método DAL Função
GetMessagesAsync Obtém uma List<Message> do banco de dados classificado pela propriedade Text.
AddMessageAsync Adiciona uma Message ao banco de dados.
DeleteAllMessagesAsync Exclui todas as entradas de Message do banco de dados.
DeleteMessageAsync Exclui uma única Message do banco de dados Id.

Os testes de unidade da DAL exigem DbContextOptions ao criar um novo AppDbContext para cada teste. Uma abordagem para criar DbContextOptions para cada teste é usar um DbContextOptionsBuilder:

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

O problema com essa abordagem é que cada teste recebe o banco de dados em qualquer estado em que o teste anterior o deixou. Isso pode ser problemático ao tentar escrever testes de unidade atômica que não interferem uns nos outros. Para forçar o AppDbContext a usar um novo contexto de banco de dados para cada teste, forneça uma instância DbContextOptions baseada em um novo provedor de serviços. O aplicativo de teste mostra como fazer isso usando seu método de classe UtilitiesTestDbContextOptions (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs):

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

Usar DbContextOptions nos testes de unidade da DAL permite que cada teste seja executado atomicamente com uma nova instância de banco de dados:

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Cada método de teste na classe DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) segue um padrão Arrange-Act-Assert semelhante:

  1. Arrange: o banco de dados está configurado para o teste e/ou o resultado esperado é definido.
  2. Act: o teste é executado.
  3. Assert: instruções assert são feitas para determinar se o resultado do teste é bem-sucedido.

Por exemplo, o método DeleteMessageAsync é responsável por remover uma única mensagem identificada por seu Id (src/RazorPagesTestSample/Data/AppDbContext.cs):

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Há dois testes para esse método. Um teste verifica se o método exclui uma mensagem quando a mensagem está presente no banco de dados. O outro método testa que o banco de dados não será alterado se a mensagem Id de exclusão não existir. O método DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound completo é mostrado abaixo:

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Primeiro, o método executa a etapa Arrange, em que a preparação para a etapa Act ocorre. As mensagens de propagação são obtidas e mantidas em seedMessages. As mensagens de propagação são salvas no banco de dados. A mensagem com um Id de 1 é definida para exclusão. Quando o método DeleteMessageAsync é executado, as mensagens esperadas devem ter todas as mensagens, exceto aquela com um Id de 1. A variável expectedMessages representa esse resultado esperado.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

O método atua: o método DeleteMessageAsync é executado passando o recId de 1:

// Act
await db.DeleteMessageAsync(recId);

Por fim, o método obtém Messages do contexto e o compara com expectedMessages declarando de que as duas são iguais:

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Para comparar se as duas List<Message> são iguais:

  • As mensagens são ordenadas por Id.
  • Os pares de mensagens são comparados na propriedade Text.

Um método de teste semelhante, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, verifica o resultado da tentativa de excluir uma mensagem que não existe. Nesse caso, as mensagens esperadas no banco de dados devem ser iguais às mensagens reais após a execução do método DeleteMessageAsync. Não deve haver nenhuma alteração no conteúdo do banco de dados:

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Testes de unidade dos métodos de modelo de página

Outro conjunto de testes de unidade é responsável por testes de métodos de modelo de página. No aplicativo de mensagens, os modelos de página Índice são encontrados na classe IndexModel em src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Método de modelo de página Função
OnGetAsync Obtém as mensagens da DAL para a interface do usuário usando o método GetMessagesAsync.
OnPostAddMessageAsync Se o ModelState for válido, chamará AddMessageAsync para adicionar uma mensagem ao banco de dados.
OnPostDeleteAllMessagesAsync Chama DeleteAllMessagesAsync para excluir todas as mensagens no banco de dados.
OnPostDeleteMessageAsync Executa DeleteMessageAsync para excluir uma mensagem com o Id especificado.
OnPostAnalyzeMessagesAsync Se uma ou mais mensagens estiverem no banco de dados, calculará o número médio de palavras por mensagem.

Os métodos de modelo de página são testados usando sete testes na classe IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Os testes usam o padrão Arrange-Assert-Act familiar. Esses testes se concentram em:

  • Determinar se os métodos seguem o comportamento correto quando o ModelState é inválido.
  • Confirmar se os métodos produzem o IActionResult correto.
  • Verificar se as atribuições de valor da propriedade são feitas corretamente.

Esse grupo de testes geralmente simula os métodos da DAL para produzir dados esperados para a etapa Act em que um método de modelo de página é executado. Por exemplo, o método GetMessagesAsync do AppDbContext é simulado para produzir saída. Quando um método de modelo de página executa esse método, a simulação retorna o resultado. Os dados não são provenientes do banco de dados. Isso cria condições de teste previsíveis e confiáveis para usar a DAL nos testes de modelo de página.

O teste OnGetAsync_PopulatesThePageModel_WithAListOfMessages mostra como o método GetMessagesAsync é simulado para o modelo de página:

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Quando o método OnGetAsync é executado na etapa Act, ele chama o método GetMessagesAsync do modelo de página.

Etapa Act do teste de unidade (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

Método OnGetAsync do modelo de página IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

O método GetMessagesAsync na DAL não retorna o resultado dessa chamada de método. A versão simulada do método retorna o resultado.

Na etapa Assert, as mensagens reais (actualMessages) são atribuídas na propriedade Messages do modelo de página. Uma verificação de tipo também é executada quando as mensagens são atribuídas. As mensagens esperadas e reais são comparadas por suas propriedades Text. O teste afirma que as duas instâncias List<Message> contêm as mesmas mensagens.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Outros testes neste grupo criam objetos de modelo de página que incluem o DefaultHttpContext, o ModelStateDictionary e um ActionContext para estabelecer o PageContext, um ViewDataDictionary e um PageContext. Eles são úteis na realização de testes. Por exemplo, o aplicativo de mensagens estabelece um erro ModelState com AddModelError para marcar que um PageResult válido é retornado quando OnPostAddMessageAsync é executado:

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Recursos adicionais