Edit: TL; DR
Заключение, которое вы должны сделать и то, и другое, потому что каждый тест служит своей цели.
Ответ:
Это хороший вопрос, который я часто задаю себе.
Во-первых, вы должны взглянуть на цель модульного теста и цель интеграционного теста.
Модульный тест :
Модульные тесты включают тестирование части приложения в изоляции от его инфраструктуры и зависимостей. При модульном тестировании контроллера logi c проверяется только содержимое одного действия, а не поведение его зависимостей или самого фреймворка.
- Такие вещи, как фильтры, маршрутизация и привязка модели не будет работать.
Тест интеграции :
Интеграционные тесты гарантируют, что компоненты приложения работают правильно уровень, который включает в себя вспомогательные инфраструктуры приложения, такие как база данных, файловая система и сеть. ASP. NET Core поддерживает интеграционные тесты с использованием инфраструктуры модульного тестирования с тестовым веб-хостом и тестовым сервером в памяти.
- Такие вещи, как фильтры, маршрутизация и привязка модели будет работать.
« Лучшая практика » следует понимать как «Имеет ценность и имеет смысл».
Вы должны спросить себя Есть ли смысл в написании теста, или я просто создаю этот тест для написания теста?
Допустим, ваш метод GetGroups()
выглядит так.
[HttpGet]
[Authorize]
public async Task<ActionResult<Group>> GetGroups()
{
var groups = await _repository.ListAllAsync();
return Ok(groups);
}
Нет смысла писать для него модульный тест! потому что то, что вы делаете, - это тестирование имитации реализации _repository
! Так какой в этом смысл ?! У метода нет logi c, и репозиторий будет только таким, каким вы его издевались, ничто в методе не говорит об обратном.
Репозиторий будет иметь свой собственный набор отдельных модульных тестов, в которых вы расскажете о реализации методов репозитория.
Теперь предположим, что ваш метод GetGroups()
- это больше, чем просто оболочка для _repository
и имеет некоторые логики c в нем.
[HttpGet]
[Authorize]
public async Task<ActionResult<Group>> GetGroups()
{
List<Group> groups;
if (HttpContext.User.IsInRole("Admin"))
groups = await _repository.FindByExpressionAsync(g => g.IsAdminGroup == true);
else
groups = await _repository.FindByExpressionAsync(g => g.IsAdminGroup == false);
//maybe some other logic that could determine a response with a different outcome...
return Ok(groups);
}
Теперь есть смысл в написании модульного теста для метода GetGroups()
, потому что результат может измениться в зависимости от высмеянного HttpContext.User
значения.
Такие атрибуты, как [Authorize]
или [ServiceFilter(….)]
, не будут активированы в модульном тесте.
.
Запись интеграционные тесты почти всегда того стоят , потому что вы хотите проверить, что будет делать процесс, когда он является частью реального приложения / системы / процесса.
Спросите себя , это используется приложением / системой? Если да , напишите интеграционный тест, потому что результат зависит от комбинации обстоятельств и критериев.
Теперь, даже если ваш GetGroups()
метод является просто оболочкой, как в первой реализации, _repository
будет указывать на реальное хранилище данных, ничего не имитируется !
Итак, теперь тест не только охватывает тот факт, что в хранилище данных есть данные (или нет), он также полагается на при фактическом подключении, HttpContext
правильно настроен и работает ли сериализация информации должным образом.
Такие вещи, как фильтры, маршрутизация и привязка модели также будут Итак, если у вас есть атрибут в вашем методе GetGroups()
, например [Authorize]
или [ServiceFilter(….)]
, он будет сработать, как ожидалось.
Я использую xUnit для тестирования, поэтому для модульного теста на контроллере я использую это.
Контроллер Unit Test:
public class MyEntityControllerShould
{
private MyEntityController InitializeController(AppDbContext appDbContext)
{
var _controller = new MyEntityController (null, new MyEntityRepository(appDbContext));
var httpContext = new DefaultHttpContext();
var context = new ControllerContext(new ActionContext(httpContext, new RouteData(), new ActionDescriptor()));
_controller.ControllerContext = context;
return _controller;
}
[Fact]
public async Task Get_All_MyEntity_Records()
{
// Arrange
var _AppDbContext = AppDbContextMocker.GetAppDbContext(nameof(Get_All_MeetUp_Records));
var _controller = InitializeController(_AppDbContext);
//Act
var all = await _controller.GetAllValidEntities();
//Assert
Assert.True(all.Value.Count() > 0);
//clean up otherwise the other test will complain about key tracking.
await _AppDbContext.DisposeAsync();
}
}
Мокер контекста, используемый для модульного тестирования.
public class AppDbContextMocker
{
/// <summary>
/// Get an In memory version of the app db context with some seeded data
/// </summary>
/// <param name="dbName"></param>
/// <returns></returns>
public static AppDbContext GetAppDbContext(string dbName)
{
//set up the options to use for this dbcontext
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseInMemoryDatabase(dbName)
.Options;
var dbContext = new AppDbContext(options);
dbContext.SeedAppDbContext();
return dbContext;
}
}
Расширение Seed.
public static class AppDbContextExtensions
{
public static void SeedAppDbContext(this AppDbContext appDbContext)
{
var myEnt = new MyEntity()
{
Id = 1,
SomeValue = "ABCD",
}
appDbContext.MyENtities.Add(myEnt);
//add more seed records etc....
appDbContext.SaveChanges();
//detach everything
foreach (var entity in appDbContext.ChangeTracker.Entries())
{
entity.State = EntityState.Detached;
}
}
}
и для тестирования интеграции: (это код из учебника, но я не могу вспомнить, где я его видел, на YouTube или Pluralsight)
настройка для TestFixture
public class TestFixture<TStatup> : IDisposable
{
/// <summary>
/// Get the application project path where the startup assembly lives
/// </summary>
string GetProjectPath(string projectRelativePath, Assembly startupAssembly)
{
var projectName = startupAssembly.GetName().Name;
var applicationBaseBath = AppContext.BaseDirectory;
var directoryInfo = new DirectoryInfo(applicationBaseBath);
do
{
directoryInfo = directoryInfo.Parent;
var projectDirectoryInfo = new DirectoryInfo(Path.Combine(directoryInfo.FullName, projectRelativePath));
if (projectDirectoryInfo.Exists)
{
if (new FileInfo(Path.Combine(projectDirectoryInfo.FullName, projectName, $"{projectName}.csproj")).Exists)
return Path.Combine(projectDirectoryInfo.FullName, projectName);
}
} while (directoryInfo.Parent != null);
throw new Exception($"Project root could not be located using application root {applicationBaseBath}");
}
/// <summary>
/// The temporary test server that will be used to host the controllers
/// </summary>
private TestServer _server;
/// <summary>
/// The client used to send information to the service host server
/// </summary>
public HttpClient HttpClient { get; }
public TestFixture() : this(Path.Combine(""))
{ }
protected TestFixture(string relativeTargetProjectParentDirectory)
{
var startupAssembly = typeof(TStatup).GetTypeInfo().Assembly;
var contentRoot = GetProjectPath(relativeTargetProjectParentDirectory, startupAssembly);
var configurationBuilder = new ConfigurationBuilder()
.SetBasePath(contentRoot)
.AddJsonFile("appsettings.json")
.AddJsonFile("appsettings.Development.json");
var webHostBuilder = new WebHostBuilder()
.UseContentRoot(contentRoot)
.ConfigureServices(InitializeServices)
.UseConfiguration(configurationBuilder.Build())
.UseEnvironment("Development")
.UseStartup(typeof(TStatup));
//create test instance of the server
_server = new TestServer(webHostBuilder);
//configure client
HttpClient = _server.CreateClient();
HttpClient.BaseAddress = new Uri("http://localhost:5005");
HttpClient.DefaultRequestHeaders.Accept.Clear();
HttpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
}
/// <summary>
/// Initialize the services so that it matches the services used in the main API project
/// </summary>
protected virtual void InitializeServices(IServiceCollection services)
{
var startupAsembly = typeof(TStatup).GetTypeInfo().Assembly;
var manager = new ApplicationPartManager
{
ApplicationParts = {
new AssemblyPart(startupAsembly)
},
FeatureProviders = {
new ControllerFeatureProvider()
}
};
services.AddSingleton(manager);
}
/// <summary>
/// Dispose the Client and the Server
/// </summary>
public void Dispose()
{
HttpClient.Dispose();
_server.Dispose();
_ctx.Dispose();
}
AppDbContext _ctx = null;
public void SeedDataToContext()
{
if (_ctx == null)
{
_ctx = _server.Services.GetService<AppDbContext>();
if (_ctx != null)
_ctx.SeedAppDbContext();
}
}
}
и используйте его так же в тесте интеграции.
public class MyEntityControllerShould : IClassFixture<TestFixture<MyEntityApp.Api.Startup>>
{
private HttpClient _HttpClient;
private const string _BaseRequestUri = "/api/myentities";
public MyEntityControllerShould(TestFixture<MyEntityApp.Api.Startup> fixture)
{
_HttpClient = fixture.HttpClient;
fixture.SeedDataToContext();
}
[Fact]
public async Task Get_GetAllValidEntities()
{
//arrange
var request = _BaseRequestUri;
//act
var response = await _HttpClient.GetAsync(request);
//assert
response.EnsureSuccessStatusCode(); //if exception is not thrown all is good
//convert the response content to expected result and test response
var result = await ContentHelper.ContentTo<IEnumerable<MyEntities>>(response.Content);
Assert.NotNull(result);
}
}
Добавлено редактирование: В заключение, вы должны сделать и то, и другое, потому что каждый тест служит своей цели.
Глядя на другие ответы, вы увидите, что консенсус состоит в том, чтобы делать и то, и другое.