我正在为我的 ASP.NET Core Web API 添加一些单元测试,我想知道是直接对 Controller 进行单元测试还是通过 HTTP 客户端进行单元测试。直接看起来大致是这样的:
[TestMethod]
public async Task GetGroups_Succeeds()
{
var controller = new GroupsController(
_groupsLoggerMock.Object,
_uowRunnerMock.Object,
_repoFactoryMock.Object
);
var groups = await controller.GetGroups();
Assert.IsNotNull(groups);
}
...而通过 HTTP 客户端大致如下所示:[TestMethod]
public void GetGroups_Succeeds()
{
HttpClient.Execute();
dynamic obj = JsonConvert.DeserializeObject<dynamic>(HttpClient.ResponseContent);
Assert.AreEqual(200, HttpClient.ResponseStatusCode);
Assert.AreEqual("OK", HttpClient.ResponseStatusMsg);
string groupid = obj[0].id;
string name = obj[0].name;
string usercount = obj[0].userCount;
string participantsjson = obj[0].participantsJson;
Assert.IsNotNull(name);
Assert.IsNotNull(usercount);
Assert.IsNotNull(participantsjson);
}
在线搜索,看起来似乎使用了两种测试 API 的方法,但我想知道最佳实践是什么。第二种方法似乎更好一些,因为它在不知道实际响应对象类型的情况下天真地测试来自 Web API 的实际 JSON 响应,但是以这种方式注入(inject)模拟存储库更加困难 - 测试必须连接到单独的本地 Web API服务器本身以某种方式配置为使用模拟对象......我想?
最佳答案
编辑:TL;DR
结论你应该同时做,因为每个测试都有不同的目的。
答:
这是一个很好的问题,我经常问自己。
首先,您必须了解单元测试的目的和集成测试的目的。
Unit Test :
Unit tests involve testing a part of an app in isolation from its infrastructure and dependencies. When unit testing controller logic, only the contents of a single action are tested, not the behaviour of its dependencies or of the framework itself.
Integration Test :
Integration tests ensure that an app's components function correctly at a level that includes the app's supporting infrastructures, such as the database, file system, and network. ASP.NET Core supports integration tests using a unit test framework with a test web host and an in-memory test server.
“最佳实践”应该被认为是“有值(value)且有意义”。
你应该问问自己编写测试有什么值(value),还是我只是为了编写测试而创建这个测试?
假设您的
GetGroups()
方法看起来像这样。[HttpGet]
[Authorize]
public async Task<ActionResult<Group>> GetGroups()
{
var groups = await _repository.ListAllAsync();
return Ok(groups);
}
为它编写单元测试没有任何值(value)!因为你正在做的是测试 mock _repository
的实现!那么这有什么意义呢?!该方法没有逻辑,并且存储库只会与您模拟的完全一样,该方法中没有任何其他建议。
存储库将有自己的一组单独的单元测试,您将在其中涵盖存储库方法的实现。
现在假设您的
GetGroups()
方法不仅仅是 _repository
的包装器并且有一些逻辑。[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()
编写单元测试是有值(value)的。方法,因为结果可能会根据 mock HttpContext.User
值(value)。属性如
[Authorize]
或 [ServiceFilter(….)]
不会在单元测试中触发。.
编写集成测试是 几乎总是值得的因为您想测试当它构成实际应用程序/系统/进程的一部分时该进程将做什么。
问问自己,应用程序/系统是否正在使用它?
如果 是的 ,编写集成测试,因为结果取决于环境和标准的组合。
现在即使你的
GetGroups()
方法只是第一个实现中的包装器,_repository
将指向一个实际的数据存储,没有什么是 mock !所以现在,测试不仅涵盖了数据存储有数据(或没有数据)这一事实,它还依赖于建立的实际连接,
HttpContext
设置是否正确以及信息的序列化是否按预期工作。过滤器、路由和模型绑定(bind)等 将也工作。
因此,如果您的
GetGroups()
上有一个属性方法,例如 [Authorize]
或 [ServiceFilter(….)]
, 它将按预期触发。我使用 xUnit 进行测试,因此对于 Controller 上的单元测试,我使用它。
Controller 单元测试:
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();
}
}
用于单元测试的 Context mocker。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;
}
}
种子扩展。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);
}
}
添加编辑:总之,您应该两者都做,因为每个测试都有不同的目的。
查看其他答案,您会发现共识是两者都做。
关于c# - 直接或通过 HTTP 客户端测试我的 Web API Controller 是最佳实践吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62815739/