asp.net 核心的集成测试(没有视图的控制器测试)

Integration tests with asp.net core (test of controllers without the views)

我正在尝试设置一个测试项目以使用身份和数据库测试我的控制器,而无需定义视图。

我有一个单元测试项目,我可以通过实例化它来测试我的控制器,将 dbContext 传递给构造函数。

 public class EventControllerTests
    {
        private readonly IEventRepository _eventRepository;
        private readonly EventController _controller;
        private readonly AppDbContext dbContext;

        const string cn = "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=EventDb;Integrated Security=True;Connect Timeout=30;Encrypt=False;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False";

        public EventControllerTests()
        {

            var options = new DbContextOptionsBuilder<EVNTS.Web.Database.AppDbContext>()
                .UseSqlServer(cn).Options;
            dbContext = new EVNTS.Web.Database.AppDbContext(options);
            // Arrange
            _eventRepository = new EventRepository(dbContext);
            _controller = new EVNTS.Web.Controllers.EventController(_eventRepository);
        }

        [Fact]
        public void ActionIndexTest()
        {
            // Act
            var result = _controller.Index(1);

            // Assert
            var model = (Event)result.Model;
            Assert.Equal(1, model.Id);
        }
    }

我有一个使用 WebApplicationFactory 的集成测试项目

 public class BasicTests : IClassFixture<WebApplicationFactory<EVNTS.Startup>>
    {
        private readonly WebApplicationFactory<EVNTS.Startup> _factory;
        private readonly HttpClient _client;

        public BasicTests(WebApplicationFactory<EVNTS.Startup> factory)
        {
            _factory = factory;
            _client = _factory.CreateClient();
        }

        [Theory]
        [InlineData("/")]
        public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
        {

            // Act
            var response = await _client.GetAsync(url);

            // Assert
            response.EnsureSuccessStatusCode(); // Status Code 200-299
            Assert.Equal("text/html; charset=utf-8",
                response.Content.Headers.ContentType.ToString());
        }


        [Fact]

        public async Task TestUserRegistration()
        {
            var s = _factory.Services.GetRequiredService<EVNTS.Web.Repositories.IEventRepository>();
            var url = "/user/register";
            var inputModel = new EVNTS.Web.ViewModels.RegisterModel()
            {
                UserName = "eric",
                Password = "123456",
                ConfirmPassword = "123456"
            };
            var sObj = JsonSerializer.Serialize(inputModel);
            var content = new StringContent(sObj, Encoding.UTF8, "application/json");
            content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
            var response = await _client.PostAsync(url, content);
            var result = response.Content.ReadAsStringAsync();
        }

    }

问题是第二个选项必须创建视图,我需要使用像 AngleSharp 这样的库来测试结果。

我想要介于两者之间的东西,我可以直接调用构造函数并测试结果视图,但 DI 会为我注入 UserManager 和 dbContext。

有什么想法吗?

干杯

这是控制器:

public class UserController : Controller
    {
        private readonly UserManager<User> _userManager;
        public UserController(UserManager<User> userManager)
        {
            _userManager = userManager;
        }

        [HttpPost]
        public async Task<IActionResult> Register([FromBody] RegisterModel model)
        {
            IdentityResult? result=null;

            if (ModelState.IsValid)
            {
                var user = await _userManager.FindByNameAsync(model.UserName);
                if (user == null)
                {
                    user = new User
                    {
                        Id = Guid.NewGuid(),
                        UserName = model.UserName,
                    };
                    result = await _userManager.CreateAsync(user, model.Password);
                }
            }
            return View(result);
        }
    }

没有中间。第一个例子是单元测试,第二个是集成测试。如果您只想查看操作的结果对象,那么您将使用单元测试方法,并且您需要模拟您的依赖项。否则,您将使用集成测试方法,并且必须处理实际模拟的服务器响应。

对于这里的价值,控制器操作应该进行集成测试,因为它们本质上依赖于许多组件组合在一起,所以您应该遵循第二种方法,解析 HTML 响应,如果必要的。

我不认为自己是如何执行单元测试的权威,但由于评论部分的字符数有限,我会在这里写下我的评论。

通常,当你发现自己很难想出一个好的单元测试(我不会在这里定义"good")的情况时,往往是因为有一些问题项目 structure/code 设计,而不是单元测试本身的实际限制(同样,并不是单元测试没有它的限制,但我认为这里不是这种情况)。

基于以上内容,我要求您包含该操作的代码,以便我们可以检查您到底要测试什么以及为什么它如此困难。

这是我的评论中主要基于意见的部分,但我将它留给你,你是想接受还是留下它。

这不是一个规则,但一个好的经验法则是控制器应该包含非常少的业务逻辑,这意味着对控制器进行单元测试应该基本上是测试请求一旦到达就可以走的不同路径控制器。

通常你会想要这样的东西:

if (!ModelState.IsValid)
{
  return BadRequest(ModelState);
}

var user = await _userManager.FindByNameAsync(model.UserName);
...
return View(result);

然后你可以用这样的东西进行单元测试:

public async Task Register_Returns_BadRequest_On_Invalid_Model()
{
  var testUsername = "TestUsername";
  var mockUserManager = new Mock<IUserManager>();
  mockUserManager.Setup(m => m.FindByNameAsync(testUsername))
    .Returns(Task.FromResult(**Not sure about this part**))
  var controller = new RegisterController(mockUserManager.Object);

  var result = await controller.Register(model: null);

  var actionResult = Assert.IsType<ActionResult<IdentityResult>>(result);
  Assert.IsType<BadRequestObjectResult>(actionResult.Result);
}

对于快乐路径,您只想检查在有效的 ModelState 上结果的类型是否为 ActionResult>

我的想法是什么:

  • 当你对控制器进行单元测试时,你不应该被实际数据打扰,这是应用程序其他部分的责任
  • 控制器单元测试应该简单明了,大多数时候你应该只测试这两种情况——无效数据 return 是某种 BadRequest,有效数据 return 是预期的回应
  • 如果您发现自己大部分时间都在 mock 太多对象,则表明您需要一些额外的抽象层。

对于您的情况,为了使我的代码结构更好并且更易于测试,我将执行以下操作:

  • 首先测试无效的 ModelState - 如果 ModelState 无效,您不想继续,这也应该包含在单元测试中。
  • 管理者应该是更高层次的抽象。 FindByNameAsyncCreateAsync 等方法更适合数据访问层。在这个动作的情况下,你的 UserManager 可以有一个像 Register 这样的方法,所以你的控制器的动作看起来像这样:

    if (!ModelState.IsValid)

    {

    return BadRequest()
    

    }

    var 结果 = _userManager.Register(model.UserName);

    return 查看(结果);

  • 现在您可以从控制器中删除 Find 和 Create 方法并创建一个 UserRepository 我认为这些方法所属的地方以及您可以单独测试它们的地方。

在此设置中,您具有这些抽象控制器 -> 管理器 -> 存储库。现在你尝试用一种方法测试这三个,我认为这是导致问题的原因。

此外,只是因为我觉得这更整洁一点,通常你使用 Service 层,如果结构太复杂,你添加管理层,这样它就变成了 Controller -> Manager -> Service - > 存储库。在你的情况下,我不确定你是否需要这种复杂性,所以也许只是为了更好的命名,将 UserManager 重命名为 UserService,这样你的代码流就是 Controller -> Service -> Repository .

另外,最后的忠告。控制器测试一直是相反的,所以如果您没有像代码的其他部分那样用单元测试来覆盖您的控制器,请不要太在意。这在某种程度上是意料之中的,我想用这个 post 告诉的主要是问题不在于如何测试,而是代码是否可以按原样进行测试,我认为可以像我所展示的那样进行改进多于。是的,我的提议也不完美,但它创建了更小的封装代码块,没有那么多依赖性,最终使它们更容易测试。当然,这不是集成测试的替代品。

希望这能给您一些启发。

有时当您想在集成测试条件下检查控制器的结果而不检查视图时,我也发现这很有用。

您可以使用依赖注入并从 WebApplicationFactory.

创建范围
        using (var serviceScope = Factory.Services.CreateScope())
        {
            var sut= serviceScope.ServiceProvider.GetService<YourController>();

        }

要完成这项工作,您必须调用 Startup.cs 中的方法 AddControllersAsServices() 以在 DI 容器中注册控制器

            services.AddControllersWithViews(options => { options.ConfigureMvcOptionsForPortalModule(); })
            .SetCompatibilityVersion(CompatibilityVersion.Version_3_0)
            .AddControllersAsServices();//add controller in DI to access it in integration testing