重新整理 .net core 实践篇——— 测试控制器[四十九]

发布时间 2023-06-15 00:46:32作者: 敖毛毛

前言

其实就是官方的例子,只是在此收录整理一下。

正文

测试控制器测试的是什么呢?

测试的是避开筛选器、路由、模型绑定,就是只测试控制器的逻辑,但是不测试器依赖项。

代码部分:

https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/mvc/controllers/testing/samples/

第一个例子:

[Fact]
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions()
{
	// arrange
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.ListAsync())
		.ReturnsAsync(GetTestSessions());

	var controller = new HomeController(mockRepo.Object);

	// act
	var result = await controller.Index();

	// assert
	var viewResult = Assert.IsType<ViewResult>(result);
	var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(viewResult.ViewData.Model);
	Assert.Equal(2, model.Count());
}

private List<BrainstormSession> GetTestSessions()
{
	var sessions = new List<BrainstormSession>();
	sessions.Add(new BrainstormSession()
	{
		DateCreated = new DateTime(2016, 7, 2),
		Id = 1,
		Name = "Test One"
	});

	sessions.Add(new BrainstormSession()
	{
		DateCreated = new DateTime(2016, 7, 1),
		Id = 2,
		Name = "Test Two"
	});

	return sessions;
}

Index_ReturnsAViewResult_WithAListOfBrainstormSessions 命名规则:

Index 是测试的方法。

ReturnsAViewResult 是结果

WithAListOfBrainstormSessions 是条件

var viewResult = Assert.IsType<ViewResult>(result);
var model = Assert.IsAssignableFrom<IEnumerable<StormSessionViewModel>>(viewResult.ViewData.Model);
Assert.Equal(2, model.Count());

有3个测试的地方:

  1. 测试结果类型是ViewResult

  2. 测试viewResult.ViewData.Model,是IEnumerable的派生

  3. 测试model的数量为2

第二个例子:

[Fact]
public async Task IndexPost_RetrunsBadRequestResult_WhenModelStateInvalid()
{
	// Arrange
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.ListAsync())
		.ReturnsAsync(GetTestSessions());
	var controller = new HomeController(mockRepo.Object);
	controller.ModelState.AddModelError("SessionName", "Required");
	var newSession = new HomeController.NewSessionModel();
	// Act
	var result = await controller.Index(newSession);
	// Assert
	var badRequestResult = Assert.IsType<BadRequestObjectResult>(result);
	Assert.IsType<SerializableError>(badRequestResult.Value);
}

进行ModelState 验证, 因为没有走mvc,那么需要自己设置验证信息。

[Fact]
public async Task IndexPost_ReturnsARedirectAndAddsSession_WhenModelStateIsValid()
{
	// Arrange
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.AddAsync(It.IsAny<BrainstormSession>()))
		.Returns(Task.CompletedTask)
		.Verifiable();
	var controller = new HomeController(mockRepo.Object);
	var newSession = new HomeController.NewSessionModel()
	{
		SessionName = "Test Name"
	};

	// Act
	var result = await controller.Index(newSession);

	// Assert
	var redirectToActionResult = Assert.IsType<RedirectToActionResult>(result);
	Assert.Null(redirectToActionResult.ControllerName);
	Assert.Equal("Index", redirectToActionResult.ActionName);
	mockRepo.Verify();
}

这里主要介绍的是mockRepo.Verify(),在mockRepo 模拟方法的时候AddAsync设置,传入的类型要是BrainstormSession。

如果不是的话,那么就会抛出异常。

it 还有很多其他的选项,比如不能为空等。

然后为什么这里是两个测试呢? 这里要介绍的是单元测试,必须要覆盖测试,分布测试不同情况。

例子3:

那么下面测试SessionController:

这里开始有些变化了,注意观察:

[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
	// Arrange
	var controller = new SessionController(sessionRepository: null);

	// Act
	var result = await controller.Index(id: null);

	// Assert
	var redirectToActionResult =
		Assert.IsType<RedirectToActionResult>(result);
	Assert.Equal("Home", redirectToActionResult.ControllerName);
	Assert.Equal("Index", redirectToActionResult.ActionName);
}

[Fact]
public async Task IndexReturnsContentWithSessionNotFoundWhenSessionNotFound()
{
	// Arrange
	int testSessionId = 1;
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
		.ReturnsAsync((BrainstormSession)null);
	var controller = new SessionController(mockRepo.Object);

	// Act
	var result = await controller.Index(testSessionId);

	// Assert
	var contentResult = Assert.IsType<ContentResult>(result);
	Assert.Equal("Session not found.", contentResult.Content);
}

public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
	var testSessionId = 1;
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
		.ReturnsAsync(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId));
	var controller = new SessionController(mockRepo.Object);

	// act
	var result = controller.Index(testSessionId);

	// Assert
	var viewResult = Assert.IsType<ViewResult>(result);
	var model = Assert.IsType<StormSessionViewModel>(
		viewResult.ViewData.Model);
	Assert.Equal("Test One", model.Name);
	Assert.Equal(2, model.DateCreated.Day);
	Assert.Equal(testSessionId, model.Id);
}

private List<BrainstormSession> GetTestSessions()
{
	var sessions = new List<BrainstormSession>();
	sessions.Add(new BrainstormSession()
	{
		DateCreated = new DateTime(2016, 7, 2),
		Id = 1,
		Name = "Test One"
	});

	sessions.Add(new BrainstormSession()
	{
		DateCreated = new DateTime(2016, 7, 1),
		Id = 2,
		Name = "Test Two"
	});

	return sessions;
}

上面不仅将方法的各种情况都覆盖了,还注意到命名:

以前命名是测试方式_测试结果_测试条件。

现在命名是一个句子来描述了,这样做的目的是使得写代码的人读起来更加通顺。

// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
	.ReturnsAsync(GetTestSessions().FirstOrDefault(
		s => s.Id == testSessionId));

这个点值得关注一下, 比如说去模拟GetByIdAsync的返回信息,是可以进行自我实现的,而不是单纯的传递一个值。

前面的例子验证了查询这块,那么第四个例子,来验证创建这块。

[Fact]
public async Task Create_ReturnsBadRequest_GivenInvalidModel()
{
    // Arrange & Act
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    var controller = new IdeasController(mockRepo.Object);
    controller.ModelState.AddModelError("error", "some error");

    // Act
    var result = await controller.Create(model: null);

    // Assert
    Assert.IsType<BadRequestObjectResult>(result);
}

[Fact]
public async Task Create_ReturnsHttpNotFound_ForInvalidSession()
{
    // Arrange
    int testSessionId = 123;
    var mockRepo = new Mock<IBrainstormSessionRepository>();
    mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
        .ReturnsAsync((BrainstormSession)null);
    var controller = new IdeasController(mockRepo.Object);

    // Act
    var result = await controller.Create(new NewIdeaModel());

    // Assert
    Assert.IsType<NotFoundObjectResult>(result);
}
[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
	// Arrange
	int testSessionId = 1;
	string testName = "test name";
	string testDescription = "test description";
	var testSession = GetTestSession();
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
		.ReturnsAsync(testSession);
	var controller = new IdeasController(mockRepo.Object);

	var newIdea = new NewIdeaModel()
	{
		Description = testDescription,
		Name = testName,
		SessionId = testSessionId
	};
	mockRepo.Setup(repo => repo.UpdateAsync(testSession))
		.Returns(Task.CompletedTask)
		.Verifiable();

	// Act
	var result = await controller.Create(newIdea);

	// Assert
	var okResult = Assert.IsType<OkObjectResult>(result);
	var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
	mockRepo.Verify();
	Assert.Equal(1, returnSession.Ideas.Count());
	Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
	Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

private BrainstormSession GetTestSession()
{
	return new BrainstormSession()
	{
		DateCreated = new DateTime(2016, 7, 1),
		Id = 2,
		Name = "Test Two",
	};
}

值得注意的是最后这个:

UpdateAsync 这个并不需要我们过多的逻辑去测试,这个是单元测试,而不需要关注依赖的细节。

然后这个有个Verifiable,这个是验证什么的呢? 验证UpdateAsync 传入的对象是testSession,如果不是的话,那么:

[Fact]
public async Task Create_ReturnsNewlyCreatedIdeaForSession()
{
	// Arrange
	int testSessionId = 1;
	string testName = "test name";
	string testDescription = "test description";
	var testSession = GetTestSession();
	var mockRepo = new Mock<IBrainstormSessionRepository>();
	mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
		.ReturnsAsync(testSession);
	var controller = new IdeasController(mockRepo.Object);

	var newIdea = new NewIdeaModel()
	{
		Description = testDescription,
		Name = testName,
		SessionId = testSessionId
	};
	mockRepo.Setup(repo => repo.UpdateAsync(GetTestSession()))
		.Returns(Task.CompletedTask)
		.Verifiable();

	// Act
	var result = await controller.Create(newIdea);

	// Assert
	var okResult = Assert.IsType<OkObjectResult>(result);
	var returnSession = Assert.IsType<BrainstormSession>(okResult.Value);
	mockRepo.Verify();
	Assert.Equal(1, returnSession.Ideas.Count());
	Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
	Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}

比如我这样写,那么会报错:

那么其实记住的是,单元测试验证的输入和输出。

比如这里的xuit, 对于验证输出是很好验证的,那么为什么使用moq这个东西呢,理由也很简单哈,那就是验证传参,也就是验证输入。

这里还有另外一个问题,当要开始Assert时候,我们应该一个怎么样的顺序去验证呢?

一个基本的思路就是:

  1. 先验证输出的类型
var actionResult = Assert.IsType<ActionResult<BrainstormSession>>(result);
var createdAtActionResult = Assert.IsType<CreatedAtActionResult>(actionResult.Result);
var returnValue = Assert.IsType<BrainstormSession>(createdAtActionResult.Value);
  1. 然后验证输入的参数
mockRepo.Verify();
  1. 最后验证细节
Assert.Equal(2, returnValue.Ideas.Count());
Assert.Equal(testName, returnValue.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnValue.Ideas.LastOrDefault().Description);

上面就是验证单元控制器的测试的基本思路了,其实的方法验证的差不多。

下一节中间件的测试,我们在很多时候要写中间件,那么这个时候就得进行测试了,这是不可避免的,那么这个怎么测试呢?