asp.net-core-webapi - 在 API 中,使用一个参数创建多个控制器构造函数
问题描述
[Route("api/[controller]")]
public class DigitalDocumentController : Controller
{
private IDigitalDocumentService digitalDocumentService;
private IDatabaseInitializer databaseInitializer;
public DigitalDocumentController(IDigitalDocumentService digitalDocumentService)
{
this.digitalDocumentService = digitalDocumentService;
}
public DigitalDocumentController(IDatabaseInitializer databaseInitializer)
{
this.databaseInitializer = databaseInitializer;
}
我希望我的项目中的两个控制器构造函数在 xUnit 测试中进行 Mock,但是我的 swagger 接口中有一个错误 {“error”:“在类型‘i2ana.Web.Controllers.DigitalDocumentController’中找到了接受所有给定参数类型的多个构造函数. 应该只有一个适用的构造函数。有人可以帮我怎么做吗?
…我想做的是测试我的数据库中名称字段的唯一性我的测试代码:
[Fact]
public void AddNotUniqueName_ReturnsNotFoundObjectResult()
{
var digitalDocument = new DigitalDocument
{
Image = new byte[] { 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 },
CreatedOn = DateTime.Today,
Id = 6,
Location = "temp",
Name = "Flower",
Tages = new List<Tag> { new Tag { Id = 1, Value = "Tag 1" }, new Tag { Id = 1, Value = "Tag 2" } }
};
// Arrange
var mockRepo = new Mock<IDatabaseInitializer>();
mockRepo.Setup(repo => repo.SeedAsync()).Returns(Task.FromResult(AddUniqueDigitalDocument(digitalDocument)));
var controller = new DigitalDocumentController(mockRepo.Object);
// Act
var result = controller.Add(digitalDocument);
// Assert
var viewResult = Assert.IsType<NotFoundObjectResult>(result);
var model = Assert.IsAssignableFrom<int>(viewResult.Value);
Assert.NotEqual(6, model);
}
“AddUniqueDigitalDocument”返回 6 只是为了测试新的 digitaldocumet 与我的初始化数据的 id 不同。
解决方案
使用依赖注入时,您应该只有一个可以满足所有依赖关系的构造函数。否则,DI 容器如何知道使用哪个构造函数?这是你的问题。使用 Microsoft.Extensions.DependencyInjection 包,并且由于这是您要注入的控制器,因此只有一种合理的方法可以解决此问题:不要注册其中一个或另一个服务,IDigitalDocumentService
或者IDatatabaseInitializer
. 如果只注册了一个,则服务集合将简单地使用它已注册服务的构造函数。
使用功能更强大的 DI 容器是可能的,您可以配置一些东西以允许它选择正确的构造函数。但是,如何做到这一点将完全取决于您最终使用的 DI 容器,因此目前关于该主题的内容不多。只需意识到默认容器 (Microsoft.Extensions.DependencyInjection) 是有意简单化的,因此如果您需要更复杂的内容,则应放入完整的 DI 容器中。
更新
您应该使用测试主机和内存数据库进行集成测试。基本方法是:
public MyTests()
{
_server = new TestServer(new WebHostBuilder().UseStartup<TestStartup>());
_context = _server.Host.Services.GetRequiredService<MyContext>();
_client = _server.CreateClient();
}
在您的应用程序中Startup
,创建一个虚拟方法:
public virtual void ConfigureDatabase(IServiceCollection services)
{
// normal database setup here, e.g.
services.AddDbContext<MyContext>(o =>
o.UseSqlServer(Configuration.GetConnectionString("Foo")));
}
然后,在 中ConfigureServices
,用调用此方法替换您的数据库设置。
最后,在您的测试项目中,创建一个TestStartup
类并覆盖该ConfigureDatabase
方法:
public class TestStartup : Startup
{
public override void ConfigureDatabase(IServiceCollection services)
{
var databaseName = Guid.NewGuid().ToString();
services.AddDbContext<MyContext>(o =>
o.UseInMemoryDatabase(databaseName));
}
}
现在,在您的测试中,您只需向测试客户端发出请求(这只是一个HttpClient
实例,因此它可以像任何其他实例一样工作HttpClient
)。首先使用适当的测试数据设置数据库,然后确保返回正确的响应:
// Arrange
_context.Add(new DigitalDocument { Name = "Foo" });
await _context.SaveChanges();
// Act
// Submit a `DigitalDocument` with the same name via `_client`
// Assert
// Inspect the response body for some indication that it was considered invalid. Or you could simply assert that no new `DigitalDocument` was created by querying `_context` (or both)
诚然,使用 API 会容易得多,就像使用 Web 应用程序一样,您总是需要进行一些 HTML 解析。但是,文档和相应的示例应用程序可以帮助您。
此外,在实际实践中,您可能希望使用测试夹具来防止必须为每个测试引导测试服务器。同样,文档已经涵盖了那里。不过,需要注意的一件事是,一旦您切换到使用夹具,您的数据库就会在测试之间持久化。要分离您的测试数据,请确保EnsureDeleted()
在每次测试之前调用您的上下文。这可以在测试类的构造函数中轻松完成:
public class MyTests : IClassFixture<WebApplicationFactory<Startup>>
{
private readonly HttpClient _client;
private readonly MyContext _context;
public MyTests(WebApplicationFactory<Startup> factory)
{
factory = factory.WithWebHostBuilder(builder => builder.UseStartup<TestStartup>());
_client = factory.CreateClient();
_context = factory.Server.Host.Services.GetRequiredService<MyContext>();
_context.EnsureDeleted();
}
不过,我什至不喜欢在我的测试中使用这么多的引导代码,所以我通常从一个夹具类继承:
public class TestServerFixture : IClassFixture<WebApplicationFactory<Startup>>
{
protected readonly HttpClient _client;
protected readonly MyContext _context;
public TestServerFixture(WebApplicationFactory<Startup> factory)
{
factory = factory.WithWebHostBuilder(builder => builder.UseStartup<TestStartup>());
_client = factory.CreateClient();
_context = factory.Server.Host.Services.GetRequiredService<MyContext>();
_context.EnsureDeleted();
}
}
然后,对于每个测试类:
public class MyTests : TestServerFixture
{
public MyTests(WebApplicationFactory<Startup> factory)
: base(factory)
{
}
这可能看起来很多,但其中大部分是一次性设置。然后,您的测试将在许多方面更准确、更健壮,甚至更容易。
推荐阅读
- angular - 如何在 Angular 6 的网络浏览器上打开本地存储的 PDF?
- scala - 以 this 作为参数调用父构造函数
- kotlin - 当你在协程范围内抛出异常时,协程范围是否可重用?
- php - PHP 对于每个循环,parse_str 接收“通知:未定义索引:标题”
- algorithm - 渐进矩阵算法
- c# - 如何在创建方法之外分配返回值
- java - 如何在容器内循环以选择硒中的值?
- r - R - 如何通过命令提示符执行脚本时显示进度
- pandas - Pandas 数据框具有正则表达式关键字
- java - Java Generic 身份验证方法应该抛出 Exception 或 RuntimeException