Как написать модульные тесты для основных контроллеров ASP.NET, которые фактически используют мой контекст базы данных?

88
9

Кажется, мало информации о том, как писать хорошие модульные тесты для действительных действий контроллера ядра ASP.NET. Любые рекомендации о том, как сделать эту работу реальной?

спросил(а) 2021-01-25T14:22:55+03:00 4 месяца, 2 недели назад
1
Решение
127

У меня есть система, которая, кажется, работает очень хорошо прямо сейчас, поэтому я решил поделиться ею и посмотреть, не поможет ли она кому-то другому. Там действительно полезная статья в документации Entity Framework, которая указывает путь. Но вот как я включил его в настоящее рабочее приложение.

1. Создайте базовое веб-приложение ASP.NET в своем решении.

Там есть много отличных статей, которые помогут вам начать работу. Документация для базовой установки и строительных лесов очень полезна. Для этого вам нужно создать веб-приложение с отдельными учетными записями пользователей, чтобы ваш ApplicationDbContext настроил работу с EntityFramework автоматически.

1a. Экранирование контроллера

Используйте информацию, содержащуюся в документации, для создания простого контроллера с основными действиями CRUD.

2. Создайте отдельную библиотеку классов для ваших модульных тестов

В своем решении создайте новую базовую библиотеку.NET и обратитесь к недавно созданному веб-приложению. В моем примере модель, которую я использую, называется Company, и она использует CompaniesController.

2а. Добавьте необходимые пакеты в тестовую библиотеку

Для этого проекта я использую xUnit как мой тестовый бегун, Moq для насмешливых объектов и FluentAssertions, чтобы сделать более содержательные утверждения. Добавьте эти три библиотеки в свой проект, используя диспетчер пакетов NuGet и/или консоль. Возможно, вам придется искать их с установленным Show Prerelease.

Вам также понадобится несколько пакетов для использования новой базы данных Sqlite-InMemory EntityFramework. Это секретный соус. Ниже перечислены имена пакетов в NuGet:

    Microsoft.Data.Sqlite Microsoft.EntityFramework Core.InMemory [выделено мной] Microsoft.EntityFramework Core.Sqlite [выделено курсивом]
3. Установите тестовое приспособление

В статье, о которой я упоминал ранее, существует простой и красивый способ настроить Sqlite для работы в качестве реляционной базы данных в памяти, с которой вы можете запускать свои тесты.

Вы захотите написать свои тестовые методы, чтобы каждый метод имел новую, чистую копию базы данных. В приведенной выше статье показано, как это сделать на разовой основе. Здесь, как я установил свой светильник как можно более сухим.

3a. Действия синхронного контроллера

Я написал следующий метод, который позволяет мне писать тесты с использованием модели Arrange/Act/Assert, причем каждый этап действует как параметр в моем тесте. Ниже приведен код для метода и соответствующих свойств класса в TestFixture который он ссылается, и, наконец, пример того, как он выглядит, чтобы вызвать код.

public class TestFixture {
public SqliteConnection ConnectionFactory() => new SqliteConnection("DataSource=:memory:");

public DbContextOptions<ApplicationDbContext> DbOptionsFactory(SqliteConnection connection) =>
new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlite(connection)
.Options;

public Company CompanyFactory() => new Company {Name = Guid.NewGuid().ToString()};

public void RunWithDatabase(
Action<ApplicationDbContext> arrange,
Func<ApplicationDbContext, IActionResult> act,
Action<IActionResult> assert)
{
var connection = ConnectionFactory();
connection.Open();

try
{
var options = DbOptionsFactory(connection);

using (var context = new ApplicationDbContext(options))
{
context.Database.EnsureCreated();
// Arrange
arrange?.Invoke(context);
}

using (var context = new ApplicationDbContext(options))
{
// Act (and pass result into assert)
var result = act.Invoke(context);
// Assert
assert.Invoke(result);
}
}
finally
{
connection.Close();
}
}
...
}

Вот как это выглядит, чтобы вызвать код для проверки метода Create в CompaniesController (я использую имена параметров, чтобы помочь мне сохранить мои выражения прямо, но вы им не нужны):

    [Fact]
public void Get_ReturnsAViewResult()
{
_fixture.RunWithDatabase(
arrange: null,
act: context => new CompaniesController(context, _logger).Create(),
assert: result => result.Should().BeOfType<ViewResult>()
);
}

Мой CompaniesController класс требует регистратор, что я макет с Moq и хранить в качестве переменного в моей TestFixture.

3b. Действия асинхронного контроллера

Конечно, многие из встроенных действий ASP.NET Core являются асинхронными. Чтобы использовать эту структуру, я написал следующий метод:

public class TestFixture {
...
public async Task RunWithDatabaseAsync(
Func<ApplicationDbContext, Task> arrange,
Func<ApplicationDbContext, Task<IActionResult>> act,
Action<IActionResult> assert)
{
var connection = ConnectionFactory();
await connection.OpenAsync();

try
{
var options = DbOptionsFactory(connection);

using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();
if (arrange != null) await arrange.Invoke(context);
}

using (var context = new ApplicationDbContext(options))
{
var result = await act.Invoke(context);
assert.Invoke(result);
}
}
finally
{
connection.Close();
}
}
}

Это почти то же самое, просто настроить с помощью асинхронных методов и ожидающих. Ниже приведен пример вызова этих методов:

    [Fact]
public async Task Post_WhenViewModelDoesNotMatchId_ReturnsNotFound()
{
await _fixture.RunWithDatabaseAsync(
arrange: async context =>
{
context.Company.Add(CompanyFactory());
await context.SaveChangesAsync();
},
act: async context => await new CompaniesController(context, _logger).Edit(1, CompanyFactory()),
assert: result => result.Should().BeOfType<NotFoundResult>()
);
}

3в. Асинхронные действия с данными

Конечно, иногда вам придется передавать данные назад и вперед между этапами тестирования. Здесь метод, который я написал, позволяет вам сделать это:

public class TestFixture {
...
public async Task RunWithDatabaseAsync(
Func<ApplicationDbContext, Task<dynamic>> arrange,
Func<ApplicationDbContext, dynamic, Task<IActionResult>> act,
Action<IActionResult, dynamic> assert)
{
var connection = ConnectionFactory();
await connection.OpenAsync();

try
{
object data;
var options = DbOptionsFactory(connection);

using (var context = new ApplicationDbContext(options))
{
await context.Database.EnsureCreatedAsync();
data = arrange != null
? await arrange?.Invoke(context)
: null;
}

using (var context = new ApplicationDbContext(options))
{
var result = await act.Invoke(context, data);
assert.Invoke(result, data);
}
}
finally
{
connection.Close();
}
}
}

И, конечно, пример того, как я использую этот код:

    [Fact]
public async Task Post_WithInvalidModel_ReturnsModelErrors()
{
await _fixture.RunWithDatabaseAsync(
arrange: async context =>
{
var data = new
{
Key = "Name",
Message = "Name cannot be null",
Company = CompanyFactory()
};
context.Company.Add(data.Company);
await context.SaveChangesAsync();
return data;
},
act: async (context, data) =>
{
var ctrl = new CompaniesController(context, _logger);
ctrl.ModelState.AddModelError(data.Key, data.Message);
return await ctrl.Edit(1, data.Company);
},
assert: (result, data) => result.As<ViewResult>()
.ViewData.ModelState.Keys.Should().Contain((string) data.Key)
);
}
Вывод

Я действительно надеюсь, что это поможет кому-то встать на ноги с С# и потрясающим новым материалом в ASP.NET Core. Если у вас есть какие-либо вопросы, критика или предложения, пожалуйста, дайте мне знать! Я все еще новичок в этом, поэтому любая конструктивная обратная связь неоценима для меня!

ответил(а) 2021-01-25T14:22:55+03:00 4 месяца, 2 недели назад
88

Вы написали много кода инфраструктуры. Тестирование контроллеров с помощью DbContext очень легко с помощью My Tested ASP.NET: https://mytestedasp.net/ Это может сделать вашу работу.

MyMvc
.Controller<MvcController>()
.WithOptions(options => options
.For<AppSettings>(settings => settings.Cache = true))
.WithSession(session => session
.WithEntry("Session", "SessionValue"))
.WithDbContext(db => db.WithEntities(entities => entities
.AddRange(SampleDataProvider.GetModels())))
.Calling(c => c.SomeAction())
.ShouldHave()
.MemoryCache(cache => cache
.ContainingEntry(entry => entry
.WithKey("CacheEntry")
.WithSlidingExpiration(TimeSpan.FromMinutes(10))
.WithValueOfType<CachedModel>()
.Passing(a => a.Id == 1)))
.AndAlso()
.ShouldReturn()
.View()
.WithModelOfType<ResponseModel>()
.Passing(m =>
{
Assert.AreEqual(1, m.Id);
Assert.AreEqual("Some property value", m.SomeProperty);
});

ответил(а) 2021-01-25T14:22:55+03:00 4 месяца, 2 недели назад
Ваш ответ
Введите минимум 50 символов
Чтобы , пожалуйста,
Выберите тему жалобы:

Другая проблема