在这篇文章中,我们将使用xUnit测试一个带有SQL后端的ASP.NET Core Web API控制器。所有的测试都将锻炼SQL数据库,而不是模拟它。这将迅速提供一个良好的覆盖率,并相信所有的Web API层都能正确工作。
要测试的控制器
我们将对控制器中的以下操作方法进行测试:
public async Task<IEnumerable<Product>> GetAll()
{
...
}
public async Task<ActionResult<Product>> GetById(Guid productId)
{
...
}
public async Task<ActionResult<Product>> Post([FromBody] Product product)
{
...
}
测试GetAll
这是我们的第一个测试:
[Fact]
public async void GetAll_ReturnsTwoProducts()
{
var configuration = GetConfig();
var sut = new ProductsController(configuration);
var result = await sut.GetAll();
Assert.Equal(2, result.Count());
}
这个控制器没有参数,所以直接调用该方法,并检查它是否返回正确的产品数量。
该控制器接收一个IConfiguration 参数,我们使用以下方法进行设置:
private IConfiguration GetConfig()
{
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", true, true)
.AddEnvironmentVariables();
return builder.Build();
}
这是在测试项目中查看一个本地appsettings.json 文件,其中包含一个指向测试数据库的连接字符串。
测试GetById
接下来是对GetById 的快乐路径测试:
[Fact]
public async void GetById_WhenKnownId_ReturnsProduct()
{
var configuration = GetConfig();
var sut = new ProductsController(configuration);
var result = await sut.GetById(Guid.Parse("E897FF55-8F3D-4154-B582-8D37D116347F"));
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var product = Assert.IsType<Product>(okResult.Value);
Assert.Equal(Guid.Parse("E897FF55-8F3D-4154-B582-8D37D116347F"), product.ProductId);
Assert.Equal("Chai", product.ProductName);
}
我们得到一个已知的产品并检查它是否被返回。
获取一个不存在的产品的测试如下:
[Fact]
public async void GetById_WhenUnknownId_Returns404()
{
var configuration = GetConfig();
var sut = new ProductsController(configuration);
var result = await sut.GetById(Guid.Parse("B051D7B5-E437-45BC-9CD1-4ED1971C2AE0"));
Assert.IsType<NotFoundResult>(result.Result);
}
我们检查一个类型为NotFoundResult 的对象是否被返回。
测试Post
对Post 的测试变得更加有趣,因为我们现在要写到数据库中。下面是我们的第一次尝试:
[Fact]
public async void Post_ReturnsProduct()
{
var configuration = GetConfig();
var sut = new ProductsController(configuration);
var result = await sut.Post(new Product() { ProductName = "Test" });
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var product = Assert.IsType<Product>(okResult.Value);
Assert.False(product.ProductId == Guid.Empty);
Assert.Equal("Test", product.ProductName);
}
测试通过了,但是当其他的测试随后运行时,我们得到以下错误:
这是因为,在Post_ReturnsProduct 测试之后,在我们的测试数据库中有三个产品。因此,在Post_ReturnsProduct 测试中,我们需要在测试结束时删除测试产品......或者永远不提交测试产品到数据库:
[Fact]
public async void Post_ReturnsProduct()
{
var configuration = GetConfig();
var sut = new ProductsController(configuration);
ActionResult<Product> result; using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) { result = await sut.Post(new Product() { ProductName = "Test" });
scope.Dispose(); }
var okResult = Assert.IsType<OkObjectResult>(result.Result);
var product = Assert.IsType<Product>(okResult.Value);
Assert.False(product.ProductId == Guid.Empty);
Assert.Equal("Test", product.ProductName);
}
我们在控制器方法调用周围使用TransactionScope ,这样它和测试就能看到数据,但数据永远不会提交到数据库中。这就避免了在测试结束后手动清理数据库的麻烦。
很好!
这篇文章的代码可以在GitHub上找到:github.com/carlrip/asp…
总结
这种方法使我们能够快速建立对API控制器关键路径的测试覆盖。TransactionScope ,在测试写到数据库的方法时很有用,因为测试不需要做任何数据清理。无论我们的数据层是使用EntityFramework、dapper还是其他什么,这个方法都能很好地发挥作用。