Модульные тесты Razor Pages в ASP.NET Core

ASP.NET Core поддерживает модульные тесты приложений Razor Pages. Тесты уровня доступа к данным (DAL) и модели страниц помогают обеспечить следующее:

  • во время создания приложения Razor Pages его части работают независимо друг от друга и вместе как единое целое;
  • классы и методы имеют ограниченные области ответственности;
  • существует дополнительная документация о том, как приложение должно себя вести;
  • регрессии, которые являются ошибками, вызванными обновлениями кода, обнаруживаются во время автоматической сборки и развертывания.

В этом разделе предполагается, что у вас есть базовое представление о приложениях Razor Pages и модульных тестах. Если вы не знакомы с приложениями Razor Pages или основами тестирования, см. следующие статьи:

Просмотреть или скачать образец кода (описание загрузки)

Пример проекта состоит из двух приложений:

App Папка проекта Description
Приложение для сообщений src/RazorPagesTestSample Позволяет пользователю добавлять сообщение, удалять одно сообщение, удалять все сообщения и анализировать сообщения (найти среднее количество слов на сообщение).
Тестирование приложения. tests/RazorPagesTestSample.Tests Используется для модульного тестирования модели DAL и индексной страницы приложения для сообщений.

Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. При использовании Visual Studio Code или командной строки выполните следующую команду в командной строке в папке tests/RazorPagesTestSample.Tests:

dotnet test

Организация приложения для сообщений

Приложение для сообщений — это система сообщений Razor Pages со следующими характеристиками:

  • Страница индекса приложения (Pages/Index.cshtml и Pages/Index.cshtml.cs) предоставляет методы модели пользовательского интерфейса и страницы для управления добавлением, удалением и анализом сообщений (найдите среднее количество слов на сообщение).
  • Сообщение описывается классом (Data/Message.cs) с двумя свойствамиMessage: Id (ключ) и Text (сообщение). Свойство Text является обязательным и ограничено 200 символами.
  • Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
  • Приложение содержит DAL в классе AppDbContext контекста базы данных (Data/AppDbContext.cs). Методы DAL помечаются как virtual, что позволяет макетирование методов для использования в тестах.
  • Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями. Эти начальные сообщения также используются в тестах.

†В разделе документации о EF Тестирование с помощью InMemory объясняется, как использовать базу данных в памяти для тестов с помощью MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.

Хотя пример приложения не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования в ASP.NET Core (пример реализует шаблон репозитория).

Организация приложения для тестирования

Приложение для тестирования — это консольное приложение в папке tests/RazorPagesTestSample.Tests.

Папка приложения для тестирования Description
UnitTests
  • DataAccessLayerTest.cs содержит модульные тесты для DAL.
  • IndexPageTests.cs содержит модульные тесты для модели страницы индекса.
Служебные программы Содержит метод TestDbContextOptions, используемый для создания новых параметров контекста базы данных для каждого модульного теста DAL, чтобы база данных сбрасывалась в базовое состояние для каждого теста.

Используемая платформа тестирования — xUnit. Используемая платформа макетирования объектов — Moq.

Модульные тесты уровня доступа к данным (DAL)

Приложение для сообщений имеет DAL с четырьмя методами, содержащимися в классе AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Каждый метод содержит один или два модульных теста в приложении для тестирования.

Метод DAL Функция
GetMessagesAsync Получает List<Message> из базы данных, отсортированной по свойству Text.
AddMessageAsync Добавляет Message в базу данных.
DeleteAllMessagesAsync Удаляет все записи Message из базы данных.
DeleteMessageAsync Удаляет одну запись Message из базы данных по полю Id.

Модульные тесты DAL требует DbContextOptions при создании нового экземпляра AppDbContext для каждого теста. Один из подходов к созданию DbContextOptions для каждого теста заключается в использовании DbContextOptionsBuilder.

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

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

Проблема этого подхода заключается в том, что каждый тест получает базу данных в том состоянии, в котором ее оставил предыдущий тест. Это может быть проблемой при попытке написания атомарных модульных тестов, которые не должны мешать друг другу. Чтобы заставить AppDbContext использовать новый контекст базы данных для каждого теста, укажите экземпляр DbContextOptions, основанный на новом поставщике служб. В приложении для тестирования показано, как это сделать с помощью метода TestDbContextOptions класса Utilities (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;
}

Использование DbContextOptions в модульных тестах DAL позволяет выполнять каждый тест атомарно с использованием нового экземпляра базы данных.

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

Каждый метод тестирования в классе DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) соответствует аналогичному шаблону "размещение-действие-утверждение".

  1. Упорядочение: база данных настроена для теста и (или) определен ожидаемый результат.
  2. Акт: выполняется тест.
  3. Утверждение: утверждения выполняются, чтобы определить, является ли результат теста успешным.

Например, метод DeleteMessageAsync отвечает за удаление одного сообщения, идентифицируемого по 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();
    }
}

Для этого метода существует два теста. Один тест проверяет, что метод удаляет сообщение, когда оно присутствует в базе данных. Другой метод проверяет, что база данных не изменяется, если не существует Id сообщения для удаления. Ниже представлен код метода DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound.

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

Сначала метод выполняет этап размещения, на котором выполняется подготовка для этапа действия. Начальные сообщения получаются и сохраняются в seedMessages. Начальные сообщения сохраняются в базу данных. Сообщение с Id, равным 1, устанавливается для удаления. После выполнения метода DeleteMessageAsync ожидаемый набор сообщений должен содержать все сообщения, кроме одного с Id, равным 1. Переменная expectedMessages содержит ожидаемый результат.

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

Метод действует: DeleteMessageAsync метод выполняется, передавая recId1следующие методы:

// Act
await db.DeleteMessageAsync(recId);

Наконец, метод получает Messages из контекста и сравнивает его с expectedMessages, проверяя, что эти два значения равны.

// 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));

Чтобы сравнить, что два List<Message> одинаковы.

  • Сообщения упорядочиваются по Id.
  • Пары сообщений сравниваются по свойству Text.

Аналогичный метод теста DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound проверяет результат попытки удаления несуществующего сообщения. В этом случае после выполнения метода DeleteMessageAsync ожидаемый набор сообщений в базе данных должен быть равным фактическим сообщениям. Содержимое базы данных не должно измениться:

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

Модульные тесты методов модели страницы

Другой набор модульных тестов отвечает за тестирование методов модели страницы. В приложении для сообщений модели индексных страниц находятся в классе IndexModel в файле src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Метод модели страницы Функция
OnGetAsync Получает сообщения из DAL для пользовательского интерфейса с помощью метода GetMessagesAsync.
OnPostAddMessageAsync Если ModelState является допустимым, вызывает метод AddMessageAsync, чтобы добавить сообщение в базу данных.
OnPostDeleteAllMessagesAsync Вызывает метод DeleteAllMessagesAsync для удаления всех сообщений в базе данных.
OnPostDeleteMessageAsync Выполняет метод DeleteMessageAsync, чтобы удалить сообщение с указанным Id.
OnPostAnalyzeMessagesAsync Если в базе данных содержится одно или несколько сообщений, вычисляет среднее количество слов на сообщение.

Методы модели страницы проверяются с помощью семи тестов в классе IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). В тестах используется привычный шаблон "размещение-утверждение-действие". Эти тесты выполняют следующие задачи:

  • определение правильности поведения методов, если ModelState является недопустимым;
  • подтверждение того, что методы выдают правильный IActionResult;
  • проверка правильности назначения значений свойств.

Эта группа тестов часто макетирует методы DAL для получения ожидаемых данных для этапа действия, в котором выполняется метод модели страницы. Например, метод GetMessagesAsync класса AppDbContext макетируется для получения выходных данных. Когда метод модели страницы выполняет этот метод, макет возвращает результат. Данные не поступают из базы данных. Это создает предсказуемые и надежные тестовые условия для использования DAL в тестовой модели страницы.

Тест OnGetAsync_PopulatesThePageModel_WithAListOfMessages показывает, как макетируется метод GetMessagesAsync для модели страницы.

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

Когда метод OnGetAsync выполняется на шаге действия, он вызывает метод GetMessagesAsync модели страницы.

Шаг действия модульного теста (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

метод OnGetAsync модели страницы IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

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

Метод GetMessagesAsync в DAL не возвращает результат для этого вызова метода. Макетная версия метода возвращает результат.

На шаге Assert (Утверждение) фактические сообщения (actualMessages) назначаются из свойства Messages модели страницы. Проверка типа также выполняется при назначении сообщений. Ожидаемые и фактические сообщения сравниваются по свойствам Text. Тест утверждает, что два экземпляра List<Message> содержат одни и те же сообщения.

// 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));

Другие тесты в этой группе создают объекты модели страницы, включающие DefaultHttpContext, ModelStateDictionary, ActionContext для установки PageContext, ViewDataDictionary и PageContext. Они полезны при проведении тестов. Например, приложение для сообщений создает ошибку ModelState с AddModelError, чтобы убедиться, что при выполнении OnPostAddMessageAsync возвращается допустимый PageResult.

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

Дополнительные ресурсы

ASP.NET Core поддерживает модульные тесты приложений Razor Pages. Тесты уровня доступа к данным (DAL) и модели страниц помогают обеспечить следующее:

  • во время создания приложения Razor Pages его части работают независимо друг от друга и вместе как единое целое;
  • классы и методы имеют ограниченные области ответственности;
  • существует дополнительная документация о том, как приложение должно себя вести;
  • регрессии, которые являются ошибками, вызванными обновлениями кода, обнаруживаются во время автоматической сборки и развертывания.

В этом разделе предполагается, что у вас есть базовое представление о приложениях Razor Pages и модульных тестах. Если вы не знакомы с приложениями Razor Pages или основами тестирования, см. следующие статьи:

Просмотреть или скачать образец кода (описание загрузки)

Пример проекта состоит из двух приложений:

App Папка проекта Description
Приложение для сообщений src/RazorPagesTestSample Позволяет пользователю добавлять сообщение, удалять одно сообщение, удалять все сообщения и анализировать сообщения (найти среднее количество слов на сообщение).
Тестирование приложения. tests/RazorPagesTestSample.Tests Используется для модульного тестирования модели DAL и индексной страницы приложения для сообщений.

Тесты можно выполнять с помощью встроенных функций тестирования интегрированной среды разработки, таких как Visual Studio. При использовании Visual Studio Code или командной строки выполните следующую команду в командной строке в папке tests/RazorPagesTestSample.Tests:

dotnet test

Организация приложения для сообщений

Приложение для сообщений — это система сообщений Razor Pages со следующими характеристиками:

  • Страница индекса приложения (Pages/Index.cshtml и Pages/Index.cshtml.cs) предоставляет методы модели пользовательского интерфейса и страницы для управления добавлением, удалением и анализом сообщений (найдите среднее количество слов на сообщение).
  • Сообщение описывается классом (Data/Message.cs) с двумя свойствамиMessage: Id (ключ) и Text (сообщение). Свойство Text является обязательным и ограничено 200 символами.
  • Сообщения хранятся с помощью базы данных Entity Framework в памяти†.
  • Приложение содержит DAL в классе AppDbContext контекста базы данных (Data/AppDbContext.cs). Методы DAL помечаются как virtual, что позволяет макетирование методов для использования в тестах.
  • Если база данных пуста при запуске приложения, то хранилище сообщений инициализируется тремя сообщениями. Эти начальные сообщения также используются в тестах.

†В разделе документации о EF Тестирование с помощью InMemory объясняется, как использовать базу данных в памяти для тестов с помощью MSTest. В этом разделе используется платформа тестирования xUnit. Концепции тестирования и реализации тестов в разных платформах тестирования похожи, но не идентичны.

Хотя пример приложения не использует шаблон репозитория и не является эффективным примером шаблона "единица работы" (UoW), Razor Pages поддерживает такие шаблоны разработки. Дополнительные сведения см. в разделах Проектирование уровня сохраняемости инфраструктуры и Логика контроллера тестирования в ASP.NET Core (пример реализует шаблон репозитория).

Организация приложения для тестирования

Приложение для тестирования — это консольное приложение в папке tests/RazorPagesTestSample.Tests.

Папка приложения для тестирования Description
UnitTests
  • DataAccessLayerTest.cs содержит модульные тесты для DAL.
  • IndexPageTests.cs содержит модульные тесты для модели страницы индекса.
Служебные программы Содержит метод TestDbContextOptions, используемый для создания новых параметров контекста базы данных для каждого модульного теста DAL, чтобы база данных сбрасывалась в базовое состояние для каждого теста.

Используемая платформа тестирования — xUnit. Используемая платформа макетирования объектов — Moq.

Модульные тесты уровня доступа к данным (DAL)

Приложение для сообщений имеет DAL с четырьмя методами, содержащимися в классе AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Каждый метод содержит один или два модульных теста в приложении для тестирования.

Метод DAL Функция
GetMessagesAsync Получает List<Message> из базы данных, отсортированной по свойству Text.
AddMessageAsync Добавляет Message в базу данных.
DeleteAllMessagesAsync Удаляет все записи Message из базы данных.
DeleteMessageAsync Удаляет одну запись Message из базы данных по полю Id.

Модульные тесты DAL требует DbContextOptions при создании нового экземпляра AppDbContext для каждого теста. Один из подходов к созданию DbContextOptions для каждого теста заключается в использовании DbContextOptionsBuilder.

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

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

Проблема этого подхода заключается в том, что каждый тест получает базу данных в том состоянии, в котором ее оставил предыдущий тест. Это может быть проблемой при попытке написания атомарных модульных тестов, которые не должны мешать друг другу. Чтобы заставить AppDbContext использовать новый контекст базы данных для каждого теста, укажите экземпляр DbContextOptions, основанный на новом поставщике служб. В приложении для тестирования показано, как это сделать с помощью метода TestDbContextOptions класса Utilities (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;
}

Использование DbContextOptions в модульных тестах DAL позволяет выполнять каждый тест атомарно с использованием нового экземпляра базы данных.

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

Каждый метод тестирования в классе DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) соответствует аналогичному шаблону "размещение-действие-утверждение".

  1. Упорядочение: база данных настроена для теста и (или) определен ожидаемый результат.
  2. Акт: выполняется тест.
  3. Утверждение: утверждения выполняются, чтобы определить, является ли результат теста успешным.

Например, метод DeleteMessageAsync отвечает за удаление одного сообщения, идентифицируемого по 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();
    }
}

Для этого метода существует два теста. Один тест проверяет, что метод удаляет сообщение, когда оно присутствует в базе данных. Другой метод проверяет, что база данных не изменяется, если не существует Id сообщения для удаления. Ниже представлен код метода DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound.

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

Сначала метод выполняет этап размещения, на котором выполняется подготовка для этапа действия. Начальные сообщения получаются и сохраняются в seedMessages. Начальные сообщения сохраняются в базу данных. Сообщение с Id, равным 1, устанавливается для удаления. После выполнения метода DeleteMessageAsync ожидаемый набор сообщений должен содержать все сообщения, кроме одного с Id, равным 1. Переменная expectedMessages содержит ожидаемый результат.

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

Метод действует: DeleteMessageAsync метод выполняется, передавая recId1следующие методы:

// Act
await db.DeleteMessageAsync(recId);

Наконец, метод получает Messages из контекста и сравнивает его с expectedMessages, проверяя, что эти два значения равны.

// 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));

Чтобы сравнить, что два List<Message> одинаковы.

  • Сообщения упорядочиваются по Id.
  • Пары сообщений сравниваются по свойству Text.

Аналогичный метод теста DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound проверяет результат попытки удаления несуществующего сообщения. В этом случае после выполнения метода DeleteMessageAsync ожидаемый набор сообщений в базе данных должен быть равным фактическим сообщениям. Содержимое базы данных не должно измениться:

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

Модульные тесты методов модели страницы

Другой набор модульных тестов отвечает за тестирование методов модели страницы. В приложении для сообщений модели индексных страниц находятся в классе IndexModel в файле src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Метод модели страницы Функция
OnGetAsync Получает сообщения из DAL для пользовательского интерфейса с помощью метода GetMessagesAsync.
OnPostAddMessageAsync Если ModelState является допустимым, вызывает метод AddMessageAsync, чтобы добавить сообщение в базу данных.
OnPostDeleteAllMessagesAsync Вызывает метод DeleteAllMessagesAsync для удаления всех сообщений в базе данных.
OnPostDeleteMessageAsync Выполняет метод DeleteMessageAsync, чтобы удалить сообщение с указанным Id.
OnPostAnalyzeMessagesAsync Если в базе данных содержится одно или несколько сообщений, вычисляет среднее количество слов на сообщение.

Методы модели страницы проверяются с помощью семи тестов в классе IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). В тестах используется привычный шаблон "размещение-утверждение-действие". Эти тесты выполняют следующие задачи:

  • определение правильности поведения методов, если ModelState является недопустимым;
  • подтверждение того, что методы выдают правильный IActionResult;
  • проверка правильности назначения значений свойств.

Эта группа тестов часто макетирует методы DAL для получения ожидаемых данных для этапа действия, в котором выполняется метод модели страницы. Например, метод GetMessagesAsync класса AppDbContext макетируется для получения выходных данных. Когда метод модели страницы выполняет этот метод, макет возвращает результат. Данные не поступают из базы данных. Это создает предсказуемые и надежные тестовые условия для использования DAL в тестовой модели страницы.

Тест OnGetAsync_PopulatesThePageModel_WithAListOfMessages показывает, как макетируется метод GetMessagesAsync для модели страницы.

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

Когда метод OnGetAsync выполняется на шаге действия, он вызывает метод GetMessagesAsync модели страницы.

Шаг действия модульного теста (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs):

// Act
await pageModel.OnGetAsync();

метод OnGetAsync модели страницы IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs):

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

Метод GetMessagesAsync в DAL не возвращает результат для этого вызова метода. Макетная версия метода возвращает результат.

На шаге Assert (Утверждение) фактические сообщения (actualMessages) назначаются из свойства Messages модели страницы. Проверка типа также выполняется при назначении сообщений. Ожидаемые и фактические сообщения сравниваются по свойствам Text. Тест утверждает, что два экземпляра List<Message> содержат одни и те же сообщения.

// 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));

Другие тесты в этой группе создают объекты модели страницы, включающие DefaultHttpContext, ModelStateDictionary, ActionContext для установки PageContext, ViewDataDictionary и PageContext. Они полезны при проведении тестов. Например, приложение для сообщений создает ошибку ModelState с AddModelError, чтобы убедиться, что при выполнении OnPostAddMessageAsync возвращается допустимый PageResult.

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

Дополнительные ресурсы