ASP.NET Core 中文文档 第四章 MVC(4.5)测试控制器逻辑
原文: Testing Controller Logic
作者: Steve Smith
翻译: 姚阿勇(Dr.Yao)
校对: 高嵩(Jack)
ASP.NET MVC 应用程序的控制器应当小巧并专注于用户界面。涉及了非 UI 事务的大控制器更难于测试和维护。
章节:
在 GitHub 上查看或下载示例
为什么要测试控制器
控制器是所有 ASP.NET Core MVC 应用程序的核心部分。因此,你应当确保它们的行为符合应用的预期。 自动化测试可以为你提供这样的保障并能够在进入生产环境之前将错误检测出来。重要的一点是,避免将非必要的职责加入你的控制器并且确保测试只关注在控制器的职责上。
控制器的逻辑应当最小化并且不要去关心业务逻辑或基础事务(如,数据访问)。要测试控制器的逻辑,而不是框架。根据有效或无效的输入去测试控制器的 行为 如何。根据其执行业务操作的返回值去测试控制器的响应。
典型的控制器职责:
- 验证
ModelState.IsValid
- 如果
ModelState
无效则返回一个错误响应 - 从持久层获取一个业务实体
- 在业务实体上执行一个操作
- 将业务实体保存到持久层
- 返回一个合适的
IActionResult
单元测试
单元测试 包括对应用中独立于基础结构和依赖项之外的某一部分的测试。对控制器逻辑进行单元测试的时候,只测试一个操作的内容,而不测试其依赖项或框架本身的行为。就是说对你的控制器操作进行测试时,要确保只聚焦于操作本身的行为。控制器单元测试避开诸如 过滤器, 路由,or 模型绑定 这些内容。由于只专注于测试某一项内容,单元测试通常编写简单而运行快捷。一组编写良好的单元测试可以无需过多开销地频繁运行。然而,单元测试并不检测组件之间交互的问题,那是集成测试的目的。
如果你在编写自定义的过滤器,路由,诸如此类,你应该对它们进行单元测试,但不是作为某个控制器操作测试的一部分。它们应该单独进行测试。
为演示单元测试,请查看下面的控制器。它显示一个头脑风暴讨论会的列表,并且可以用 POST 请求创建新的头脑风暴讨论会:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
namespace TestingControllersSample.Controllers
{
public class HomeController : Controller // 手动高亮
{
private readonly IBrainstormSessionRepository _sessionRepository;
public HomeController(IBrainstormSessionRepository sessionRepository) // 手动高亮
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index() // 手动高亮
{
var sessionList = await _sessionRepository.ListAsync();
var model = sessionList.Select(session => new StormSessionViewModel()
{
Id = session.Id,
DateCreated = session.DateCreated,
Name = session.Name,
IdeaCount = session.Ideas.Count
});
return View(model);
}
public class NewSessionModel
{
[Required]
public string SessionName { get; set; }
}
[HttpPost] // 手动高亮
public async Task<IActionResult> Index(NewSessionModel model) // 手动高亮
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
await _sessionRepository.AddAsync(new BrainstormSession()
{
DateCreated = DateTimeOffset.Now,
Name = model.SessionName
});
return RedirectToAction("Index");
}
}
}
这个控制器遵循显式依赖原则,期望依赖注入为其提供一个 IBrainstormSessionRepository
的实例。这样就非常容易用一个 Mock 对象框架来进行测试,比如 Moq 。HTTP GET Index
方法没有循环或分支,只是调用了一个方法。要测试这个 Index
方法,我们需要验证是否返回了一个 ViewResult
,其中包含一个来自存储库的 List
方法的 ViewModel
。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;
namespace TestingControllersSample.Tests.UnitTests
{
public class HomeControllerTests
{
[Fact] // 手动高亮
public async Task Index_ReturnsAViewResult_WithAListOfBrainstormSessions() // 手动高亮
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(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());
}
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(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);
}
[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();
}
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;
}
}
}
HTTP POST Index
方法(下面所示)应当验证:
- 当
ModelState.IsValid
为false
时,操作方法返回一个包含适当数据的ViewResult
。 - 当
ModelState.IsValid
为true
时,存储库的Add
方法被调用,然后返回一个包含正确变量内容的RedirectToActionResult
。
[Fact]
public async Task IndexPost_ReturnsBadRequestResult_WhenModelStateIsInvalid()
{
// Arrange
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.ListAsync()).Returns(Task.FromResult(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); // 手动高亮
}
[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();
}
第一个测试确定当 ModelState
无效时,返回一个与 GET
请求一样的 ViewResult
。注意,测试不会尝试传递一个无效模型进去。那样是没有作用的,因为模型绑定并没有运行 - 我们只是直接调用了操作方法。然而,我们并不想去测试模型绑定 —— 我们只是在测试操作方法里的代码行为。最简单的方法就是在 ModelState
中添加一个错误。
第二个测试验证当 ModelState
有效时,新的 BrainstormSession
被添加(通过存储库),并且该方法返回一个带有预期属性值的 RedirectToActionResult
。未被执行到的 mock 调用通常就被忽略了,但是在设定过程的最后调用 Verifiable
则允许其在测试中被验证。这是通过调用 mockRepo.Verify
实现的。
这个例子中所采用的 Moq 库能够简单地混合可验证的,“严格的”及带有不可验证mock(也称为 “宽松的” mock 或 stub)的mock。了解更多关于 使用 Moq 自定义 Mock 行为。
应用程序里的另外一个控制器显示指定头脑风暴讨论会的相关信息。它包含一些处理无效 id 值的逻辑:
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.ViewModels;
namespace TestingControllersSample.Controllers
{
public class SessionController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public SessionController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
public async Task<IActionResult> Index(int? id)
{
if (!id.HasValue) // 手动高亮
{ // 手动高亮
return RedirectToAction("Index", "Home"); // 手动高亮
} // 手动高亮
var session = await _sessionRepository.GetByIdAsync(id.Value);
if (session == null) // 手动高亮
{ // 手动高亮
return Content("Session not found."); // 手动高亮
} // 手动高亮
var viewModel = new StormSessionViewModel()
{
DateCreated = session.DateCreated,
Name = session.Name,
Id = session.Id
};
return View(viewModel);
}
}
}
这个控制器操作有三种情况要测试,每条 return
语句一种:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Controllers;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.ViewModels;
using Xunit;
namespace TestingControllersSample.Tests.UnitTests
{
public class SessionControllerTests
{
[Fact]
public async Task IndexReturnsARedirectToIndexHomeWhenIdIsNull()
{
// Arrange
var controller = new SessionController(sessionRepository: null);
// Act
var result = await controller.Index(id: null);
// Arrange
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))
.Returns(Task.FromResult((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); // 手动高亮
}
[Fact]
public async Task IndexReturnsViewResultWithStormSessionViewModel()
{
// Arrange
int testSessionId = 1;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult(GetTestSessions().FirstOrDefault(s => s.Id == testSessionId)));
var controller = new SessionController(mockRepo.Object);
// Act
var result = await 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;
}
}
}
这个应用程序以 Web API (一个头脑风暴讨论会的意见列表以及一个给讨论会添加新意见的方法)的形式公开功能:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
namespace TestingControllersSample.Api
{
[Route("api/ideas")]
public class IdeasController : Controller
{
private readonly IBrainstormSessionRepository _sessionRepository;
public IdeasController(IBrainstormSessionRepository sessionRepository)
{
_sessionRepository = sessionRepository;
}
[HttpGet("forsession/{sessionId}")] // 手动高亮
public async Task<IActionResult> ForSession(int sessionId) // 手动高亮
{
var session = await _sessionRepository.GetByIdAsync(sessionId);
if (session == null)
{
return NotFound(sessionId); // 手动高亮
}
var result = session.Ideas.Select(idea => new IdeaDTO()// 手动高亮
{ // 手动高亮
Id = idea.Id, // 手动高亮
Name = idea.Name, // 手动高亮
Description = idea.Description, // 手动高亮
DateCreated = idea.DateCreated // 手动高亮
}).ToList(); // 手动高亮
return Ok(result);
}
[HttpPost("create")] // 手动高亮
public async Task<IActionResult> Create([FromBody]NewIdeaModel model) // 手动高亮
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState); // 手动高亮
}
var session = await _sessionRepository.GetByIdAsync(model.SessionId);
if (session == null)
{
return NotFound(model.SessionId); // 手动高亮
}
var idea = new Idea()
{
DateCreated = DateTimeOffset.Now,
Description = model.Description,
Name = model.Name
};
session.AddIdea(idea);
await _sessionRepository.UpdateAsync(session);
return Ok(session); // 手动高亮
}
}
}
ForSession
方法返回一个 IdeaDTO
类型的列表,该类型有着符合 JavaScript 惯例的驼峰命名法的属性名。从而避免直接通过 API 调用返回你业务领域的实体,因为通常它们都包含了 API 客户端并不需要的更多数据,而且它们将你的应用程序的内部领域模型与外部公开的 API 不必要地耦合起来。可以手动将业务领域实体与你想要返回的类型连接映射起来(使用这里展示的 LINQ Select
),或者使用诸如 AutoMapper的类库。
Create
和 ForSession
API 方法的单元测试:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Moq;
using TestingControllersSample.Api;
using TestingControllersSample.ClientModels;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using Xunit;
namespace TestingControllersSample.Tests.UnitTests
{
public class ApiIdeasControllerTests
{
[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)) // 手动高亮
.Returns(Task.FromResult((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 = 123;
string testName = "test name";
string testDescription = "test description";
var testSession = GetTestSession();
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId)) // 手动高亮
.Returns(Task.FromResult(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(2, returnSession.Ideas.Count());
Assert.Equal(testName, returnSession.Ideas.LastOrDefault().Name);
Assert.Equal(testDescription, returnSession.Ideas.LastOrDefault().Description);
}
[Fact]
public async Task ForSession_ReturnsHttpNotFound_ForInvalidSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId))
.Returns(Task.FromResult((BrainstormSession)null));
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var notFoundObjectResult = Assert.IsType<NotFoundObjectResult>(result);
Assert.Equal(testSessionId, notFoundObjectResult.Value);
}
[Fact]
public async Task ForSession_ReturnsIdeasForSession()
{
// Arrange
int testSessionId = 123;
var mockRepo = new Mock<IBrainstormSessionRepository>();
mockRepo.Setup(repo => repo.GetByIdAsync(testSessionId)).Returns(Task.FromResult(GetTestSession()));
var controller = new IdeasController(mockRepo.Object);
// Act
var result = await controller.ForSession(testSessionId);
// Assert
var okResult = Assert.IsType<OkObjectResult>(result);
var returnValue = Assert.IsType<List<IdeaDTO>>(okResult.Value);
var idea = returnValue.FirstOrDefault();
Assert.Equal("One", idea.Name);
}
private BrainstormSession GetTestSession()
{
var session = new BrainstormSession()
{
DateCreated = new DateTime(2016, 7, 2),
Id = 1,
Name = "Test One"
};
var idea = new Idea() { Name = "One" };
session.AddIdea(idea);
return session;
}
}
}
如前所述,要测试这个方法在 ModelState
无效时的行为,可以将一个模型错误作为测试的一部分添加到控制器。不要在单元测试尝试测试模型验证或者模型绑定 —— 仅仅测试应对特定 ModelState
值的时候,你的操作方法的行为。
第二项测试需要存储库返回 null ,因此将模拟的存储库配置为返回 null 。没有必要去创建一个测试数据库(内存中的或其他的)并构建一条能返回这个结果的查询 —— 就像展示的那样,一行代码就可以了。.
最后一项测试验证存储库的 Update
方法是否被调用。像我们之前做过的那样,在调用 mock 时调用了 Verifiable
,然后模拟存储库的 Verify
方法被调用,用以确认可验证的方法已被执行。确保 Update
保存了数据并不是单元测试的职责;那是集成测试做的事。
集成测试
集成测试是为了确保你应用程序里各独立模块能够正确地一起工作。通常,能进行单元测试的东西,都能进行集成测试,但反之则不行。不过,集成测试往往比单元测试慢得多。因此,最好尽量采用单元测试,在涉及到多方合作的情况下再进行集成测试。
尽管 mock 对象仍然有用,但在集成测试中很少用到它们。在单元测试中,mock 对象是一种有效的方式,根据测试目的去控制测试单元外的合作者应当有怎样的行为。在集成测试中,则采用真实的合作者来确定整个子系统能够正确地一起工作。
应用程序状态
在执行集成测试的时候,一个重要的考虑因素就是如何设置你的应用程序的状态。各个测试需要独立地运行,所以每个测试都应该在已知状态下随应用程序启动。如果你的应用没有使用数据库或者任何持久层,这可能不是个问题。然而,大多数真实的应用程序都会将它们的状态持久化到某种数据存储中,所以某个测试对其有任何改动都可能影响到其他测试,除非重置了数据存储。使用内置的 TestServer
,它可以直接托管我们集成测试中的 ASP.NET Core 应用程序,但又无须对我们将使用的数据授权访问。如果你正在使用真实的数据库,一种方法是让应用程序连接到测试数据库,你的测试可以访问它并且确保在每个测试执行之前会重置到一个已知的状态。
在这个示例应用程序里,我采用了 Entity Framework Core 的 InMemoryDatabase 支持,因此我可以直接把我的测试项目连接到它。实际上,我在应用程序的 Startup
类里公开了一个 InitializeDatabase
方法,我可以在开发( Development
)环境中启动应用程序的时候调用这个方法。我的集成测试只要把环境设置为 Development
,就能自动从中受益。我不需要担心重置数据库,因为 InMemoryDatabase 会在应用程序每次重启的时候重置。
The Startup
class:
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TestingControllersSample.Core.Interfaces;
using TestingControllersSample.Core.Model;
using TestingControllersSample.Infrastructure;
namespace TestingControllersSample
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>( // 手动高亮
optionsBuilder => optionsBuilder.UseInMemoryDatabase());// 手动高亮
services.AddMvc();
services.AddScoped<IBrainstormSessionRepository,
EFStormSessionRepository>();
}
public void Configure(IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(LogLevel.Warning);
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
var repository = app.ApplicationServices.GetService<IBrainstormSessionRepository>();// 手动高亮
InitializeDatabaseAsync(repository).Wait(); // 手动高亮
}
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
public async Task InitializeDatabaseAsync(IBrainstormSessionRepository repo) // 手动高亮
{
var sessionList = await repo.ListAsync();
if (!sessionList.Any())
{
await repo.AddAsync(GetTestSession());
}
}
public static BrainstormSession GetTestSession() // 手动高亮
{
var session = new BrainstormSession()
{
Name = "Test Session 1",
DateCreated = new DateTime(2016, 8, 1)
};
var idea = new Idea()
{
DateCreated = new DateTime(2016, 8, 1),
Description = "Totally awesome idea",
Name = "Awesome idea"
};
session.AddIdea(idea);
return session;
}
}
}
在下面的集成测试中,你会看到 GetTestSession
方法被频繁使用。
访问视图
每一个集成测试类都会配置 TestServer
来运行 ASP.NET Core 应用程序。默认情况下,TestServer
在其运行的目录下承载 Web 应用程序 —— 在本例中,就是测试项目文件夹。因此,当你尝试测试返回 ViewResult
的控制器操作的时候,你会看见这样的错误:
未找到视图 “Index”。已搜索以下位置:
(位置列表)
要修正这个问题,你需要配置服务器使其采用 Web 项目的 ApplicationBasePath
和 ApplicationName
。这在所示的集成测试类中调用 UseServices
完成的:
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;
namespace TestingControllersSample.Tests.IntegrationTests
{
public class HomeControllerTests : IClassFixture<TestFixture<TestingControllersSample.Startup>>
{
private readonly HttpClient _client;
public HomeControllerTests(TestFixture<TestingControllersSample.Startup> fixture)
{
_client = fixture.Client;
}
[Fact]
public async Task ReturnsInitialListOfBrainstormSessions()
{
// Arrange
var testSession = Startup.GetTestSession();
// Act
var response = await _client.GetAsync("/");
// Assert
response.EnsureSuccessStatusCode();
var responseString = await response.Content.ReadAsStringAsync();
Assert.True(responseString.Contains(testSession.Name));
}
[Fact]
public async Task PostAddsNewBrainstormSession()
{
// Arrange
string testSessionName = Guid.NewGuid().ToString();
var data = new Dictionary<string, string>();
data.Add("SessionName", testSessionName);
var content = new FormUrlEncodedContent(data);
// Act
var response = await _client.PostAsync("/", content);
// Assert
Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
Assert.Equal("/", response.Headers.Location.ToString());
}
}
}
在上面的测试中,responseString
从视图获取真实渲染的 HTML ,可以用来检查确认其中是否包含期望的结果。
API 方法
如果你的应用程序有公开的 Web API,采用自动化测试来确保它们按期望执行是个好主意。内置的 TestServer
便于测试 Web API。如果你的 API 方法使用了模型绑定,那么你应该始终检查 ModelState.IsValid
,另外确认你的模型验证工作是否正常应当在集成测试里进行。
下面一组测试针对上文所示的 ideasController里的 Create
方法:
[Fact]
public async Task CreatePostReturnsBadRequestForMissingNameValue()
{
// Arrange
var newIdea = new NewIdeaDto("", "Description", 1);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task CreatePostReturnsBadRequestForMissingDescriptionValue()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "", 1);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooSmall()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 0);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task CreatePostReturnsBadRequestForSessionIdValueTooLarge()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 1000001);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
[Fact]
public async Task CreatePostReturnsNotFoundForInvalidSession()
{
// Arrange
var newIdea = new NewIdeaDto("Name", "Description", 123);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task CreatePostReturnsCreatedIdeaWithCorrectInputs()
{
// Arrange
var testIdeaName = Guid.NewGuid().ToString();
var newIdea = new NewIdeaDto(testIdeaName, "Description", 1);
// Act
var response = await _client.PostAsJsonAsync("/api/ideas/create", newIdea);
// Assert
response.EnsureSuccessStatusCode();
var returnedSession = await response.Content.ReadAsJsonAsync<BrainstormSession>();
Assert.Equal(2, returnedSession.Ideas.Count);
Assert.True(returnedSession.Ideas.Any(i => i.Name == testIdeaName));
}
[Fact]
public async Task ForSessionReturnsNotFoundForBadSessionId()
{
// Arrange & Act
var response = await _client.GetAsync("/api/ideas/forsession/500");
// Assert
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
public async Task ForSessionReturnsIdeasForValidSessionId()
{
// Arrange
var testSession = Startup.GetTestSession();
// Act
var response = await _client.GetAsync("/api/ideas/forsession/1");
// Assert
response.EnsureSuccessStatusCode();
var ideaList = JsonConvert.DeserializeObject<List<IdeaDTO>>(
await response.Content.ReadAsStringAsync());
var firstIdea = ideaList.First();
Assert.Equal(testSession.Ideas.First().Name, firstIdea.Name);
}
}
不同于对返回 HTML 视图的操作的集成测试,有返回值的 Web API 方法通常能够反序列化为强类型对象,就像上面所示的最后一个测试。在此例中,该测试将返回值反序列化为一个 BrainstormSession
实例,然后再确认意见是否被正确添加到了意见集合里。
你可以在sample project这篇文章里找到更多的集成测试示例。
ASP.NET Core 中文文档 第四章 MVC(4.5)测试控制器逻辑的更多相关文章
- ASP.NET Core 中文文档 第四章 MVC(01)ASP.NET Core MVC 概览
原文:Overview of ASP.NET Core MVC 作者:Steve Smith 翻译:张海龙(jiechen) 校对:高嵩 ASP.NET Core MVC 是使用模型-视图-控制器(M ...
- ASP.NET Core 中文文档 第四章 MVC(4.2)控制器操作的路由
原文:Routing to Controller Actions 作者:Ryan Nowak.Rick Anderson 翻译:娄宇(Lyrics) 校对:何镇汐.姚阿勇(Dr.Yao) ASP.NE ...
- ASP.NET Core 中文文档 第四章 MVC(3.6.1 )Tag Helpers 介绍
原文:Introduction to Tag Helpers 作者:Rick Anderson 翻译:刘浩杨 校对:高嵩(Jack) 什么是 Tag Helpers? Tag Helpers 提供了什 ...
- ASP.NET Core 中文文档 第四章 MVC(3.8)视图中的依赖注入
原文:Dependency injection into views 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:孟帅洋(书缘) ASP.NET Core 支持在视图中使用 依赖 ...
- ASP.NET Core 中文文档 第四章 MVC(4.6)Areas(区域)
原文:Areas 作者:Dhananjay Kumar 和 Rick Anderson 翻译:耿晓亮(Blue) 校对:许登洋(Seay) Areas 是 ASP.NET MVC 用来将相关功能组织成 ...
- ASP.NET Core 中文文档 第四章 MVC(4.4)依赖注入和控制器
原文: Dependency Injection and Controllers 作者: Steve Smith 翻译: 刘浩杨 校对: 孟帅洋(书缘) ASP.NET Core MVC 控制器应通过 ...
- ASP.NET Core 中文文档 第四章 MVC(4.1)Controllers, Actions 和 Action Results
原文:Controllers, Actions, and Action Results 作者:Steve Smith 翻译:姚阿勇(Dr.Yao) 校对:许登洋(Seay) Action 和 acti ...
- ASP.NET Core 中文文档 第四章 MVC(3.9)视图组件
作者: Rick Anderson 翻译: 娄宇(Lyrics) 校对: 高嵩 章节: 介绍视图组件 创建视图组件 调用视图组件 演练:创建一个简单的视图组件 附加的资源 查看或下载示例代码 介绍视图 ...
- ASP.NET Core 中文文档 第四章 MVC(3.7 )局部视图(partial)
原文:Partial Views 作者:Steve Smith 翻译:张海龙(jiechen).刘怡(AlexLEWIS) 校对:许登洋(Seay).何镇汐.魏美娟(初见) ASP.NET Core ...
随机推荐
- Connect() 2016 大会的主题 ---微软大法好
文章首发于微信公众号"dotnet跨平台",欢迎关注,可以扫页面左面的二维码. 今年 Connect 大会的主题是 Big possibilities. Bold technolo ...
- [C#] 软硬结合第二篇——酷我音乐盒的逆天玩法
1.灵感来源: LZ是纯宅男,一天从早上8:00起一直要呆在电脑旁到晚上12:00左右吧~平时也没人来闲聊几句,刷空间暑假也没啥动态,听音乐吧...~有些确实不好听,于是就不得不打断手头的工作去点击下 ...
- 从Script到Code Blocks、Code Behind到MVC、MVP、MVVM
刚过去的周五(3-14)例行地主持了技术会议,主题正好是<UI层的设计模式——从Script.Code Behind到MVC.MVP.MVVM>,是前一天晚上才定的,中午花了半小时准备了下 ...
- js中参数不对应问题
因为js是一种弱类型的编程语言,对数据类型的要求没有其他编程语言的要求严格,所以在定义函数的时候不需要像java和C#一样对其传入参数的类型进行定义.那么传入参数的个数有没有影响呢?今天小猪就做了个实 ...
- AFNetworking 3.0 源码解读(九)之 AFNetworkActivityIndicatorManager
让我们的APP像艺术品一样优雅,开发工程师更像是一名匠人,不仅需要精湛的技艺,而且要有一颗匠心. 前言 AFNetworkActivityIndicatorManager 是对状态栏中网络激活那个小控 ...
- WebGIS中等值线前端生成绘制简析
文章版权由作者李晓晖和博客园共有,若转载请于明显处标明出处:http://www.cnblogs.com/naaoveGIS/ 1.背景 等值线是GIS制图中常见的功能,一般有两种思路:一种是先进行插 ...
- 《LoadRunner12七天速成宝典》签售会2016-12-17北京
报名地址: http://www.after615.com/actives/s?id=3141&time=1480042829608&sign=9ac8e25e9ab3cf57f613 ...
- atitit.细节决定成败的适合情形与缺点
atitit.细节决定成败的适合情形与缺点 1. 在理论界有两种观点:一种是"细节决定成败",另一种是"战略决定成败".1 1.1. 格局决定成败,方向决定成败 ...
- Kafka:主要参数详解(转)
原文地址:http://kafka.apache.org/documentation.html ############################# System ############### ...
- linux启动SSH及开机自动启动
本文地址 分享提纲: 1.查看是否启动 2. 设置自动启动 1.[查看是否启动] 启动SSH服务 “/etc/init.d/sshd start”.然后用netstat -antulp | grep ...