首页 > 解决方案 > 在 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 不同。

标签: asp.net-core-webapi

解决方案


使用依赖注入时,您应该只有一个可以满足所有依赖关系的构造函数。否则,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)
     {
     }

这可能看起来很多,但其中大部分是一次性设置。然后,您的测试将在许多方面更准确、更健壮,甚至更容易。


推荐阅读