RESTful 第 8 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第二部分)






4.98/5 (27投票s)
在本文中,我们将学习如何为 WebAPI 控制器(即 REST 的实际端点)编写单元测试。
目录
- 目录
- 引言
- 路线图
- 设置解决方案
- 测试 WebAPI
- 结论
引言
在上一篇文章中,我解释了如何为业务服务层编写单元测试。在本文中,我们将学习如何为 WebAPI 控制器(即 REST 的实际端点)编写单元测试。我将使用 NUnit 和 Moq 框架为控制器方法编写测试用例。我已经解释了如何安装 NUnit 和配置单元测试。我的上一篇文章还解释了用于编写单元测试的 NUnit 属性。请在阅读本文之前,先阅读本系列的上一篇文章。
路线图
以下是我为逐步学习WebAPI设置的路线图
- RESTful 第 1 天:使用 Entity Framework、通用仓库模式和工作单元的 Web API 企业级应用程序架构。
- RESTful 第 2 天:使用 Unity 容器和引导程序在 Web API 中使用依赖注入实现控制反转。
- RESTful 第 3 天:使用 Unity 容器和托管可扩展性框架 (MEF) 在 Asp.net Web API 中使用控制反转和依赖注入解决依赖关系的依赖关系。
- RESTful 第 4 天:使用 MVC 4 Web API 中的属性路由进行自定义 URL 重写/路由。
- RESTful 第 5 天:使用 Action Filter 在 Web API 中实现基本身份验证和基于令牌的自定义授权。
- RESTful 第 6 天:使用 Action Filter、异常过滤器和 NLog 在 Web API 中进行请求日志记录和异常处理/日志记录。
- RESTful 第 7 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 1 部分)。
- RESTful 第 8 天:使用 NUnit 和 Moq 框架在 WebAPI 中进行单元测试和集成测试(第 2 部分)。
- RESTful Day #9:在 ASP.NET Web APIs 中扩展 OData 支持。
- RESTful 第 10 天:在 Visual Studio 2010 中创建带有 CRUD 操作的自托管 ASP.NET WebAPI
我将特意使用 Visual Studio 2010 和 .NET Framework 4.0,因为在 .NET Framework 4.0 中,有些实现很难找到,但我会通过展示如何实现它们来使其更容易。
设置解决方案
当您从上一篇文章中获取代码库并在 Visual Studio 中打开它时,您会看到项目结构如下面的图像所示。
该解决方案包含 WebAPI 应用程序和相关项目。有两个新添加的项目,名为 BusinessServices.Tests 和 TestHelper。我们将使用 TestHelper 项目及其类来编写 WebAPI 单元测试,就像我们用于编写业务服务单元测试一样。
测试 WebAPI
步骤 1:测试项目
在现有的 Visual Studio 中添加一个简单的类库,并将其命名为 ApiController.Tests。打开 Tools->Library Packet Manager->Packet manager Console 以打开程序包管理器控制台窗口。在继续之前,我们需要安装与业务服务之前相同的程序包。
步骤 2:安装 NUnit 包
在程序包管理器控制台中,将 ApiController.Tests 选择为默认项目,然后输入命令“Install-Package NUnit –Version 2.6.4”。运行命令时,它会提示 NUnit 已安装。这是因为我们已为此包安装到 BusinessServices.Tests 项目,但再次为新项目(在本例中为 ApiController.Tests)执行此操作不会再次安装它,而是会为 ApiController.Tests 项目添加对 NUnit 框架库的引用,并在 packages.config 中添加条目。
成功安装后,您可以在项目引用中看到 DLL 引用,即 nunit.framework,
步骤 3:安装 Moq 框架
以与步骤 2 中解释的相同方式在同一项目上安装框架。输入命令“Install-Package Moq”。
步骤 4:安装 Entity Framework
Install-Package EntityFramework –Version 5.0.0
步骤 5:Newtonsoft.Json
Json.NET 是 .NET 中一个流行的、高性能的 JSON 框架。我们将使用它来序列化/反序列化请求和响应。
Install-Package Newtonsoft.Json -Version 4.5.11
我们的 packages.config,即自动添加到项目中的文件,如下所示:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net40" />
<package id="Moq" version="4.2.1510.2205" targetFramework="net40" />
<package id="Newtonsoft.Json" version="4.5.11" targetFramework="net40" />
<package id="NUnit" version="2.6.4" targetFramework="net40" />
</packages>
步骤 6:引用
将 BusinessEntities、BusinessServices、DataModel、TestsHelper、WebApi 项目的引用添加到此项目。
ProductController 测试
我们将从设置项目和准备测试先决条件开始,然后逐步进行实际测试。
测试设置
在 ApiController.Tests 项目中添加一个名为 *ProductControllerTest.cs* 的新类。
声明变量
定义我们将在类中使用的私有变量来编写测试。
#region Variables
private IProductServices _productService;
private ITokenServices _tokenService;
private IUnitOfWork _unitOfWork;
private List<Product> _products;
private List<Token> _tokens;
private GenericRepository<Product> _productRepository;
private GenericRepository<Token> _tokenRepository;
private WebApiDbEntities _dbEntities;
private HttpClient _client;
private HttpResponseMessage _response;
private string _token;
private const string ServiceBaseURL = "https://:50875/";
#endregion
变量声明是不言自明的,其中 `_productService` 将保存 ProductServices 的模拟对象,`_tokenService` 将保存 TokenServices 的模拟对象,`_unitOfWork` 用于 UnitOfWork 类,`_products` 将保存 TestHelper 项目的 DataInitializer 类中的模拟产品,`__tokens` 将保存 TestHelper 项目的 DataInitializer 类中的模拟令牌,`_productRepository`、`tokenRepository` 和 `_dbEntities` 分别保存 DataModel 项目中 Product Repository、Token Repository 和 WebAPIDbEntities 的模拟对象。
由于 WebAPI 旨在返回 HttpResponse 格式的响应,因此声明了 `_response` 来存储返回的响应,我们可以基于该响应进行断言。`_token` 保存成功认证后的令牌值。`_client` 和 ServiceBaseURL 在本文的上下文中可能不是必需的,但您可以使用它们来编写集成测试,这些测试会故意使用实际的 API URL 并对实际数据库进行测试。
编写测试夹具设置
编写测试夹具设置方法,并在顶部添加 [TestFixtureSetUp
] 属性,此方法在测试执行时仅运行一次。
[TestFixtureSetUp]
public void Setup()
{
_products = SetUpProducts();
_tokens = SetUpTokens();
_dbEntities = new Mock<WebApiDbEntities>().Object;
_tokenRepository = SetUpTokenRepository();
_productRepository = SetUpProductRepository();
var unitOfWork = new Mock<IUnitOfWork>();
unitOfWork.SetupGet(s => s.ProductRepository).Returns(_productRepository);
unitOfWork.SetupGet(s => s.TokenRepository).Returns(_tokenRepository);
_unitOfWork = unitOfWork.Object;
_productService = new ProductServices(_unitOfWork);
_tokenService = new TokenServices(_unitOfWork);
_client = new HttpClient { BaseAddress = new Uri(ServiceBaseURL) };
var tokenEntity = _tokenService.GenerateToken(1);
_token = tokenEntity.AuthToken;
_client.DefaultRequestHeaders.Add("Token", _token);
}
此方法的作用与我们为业务服务编写的方法相同。这里 `SetupProducts()` 将获取模拟产品列表,`SetupTokens()` 将获取模拟令牌列表。我们还尝试设置 Token 和 Product 存储库的模拟对象,然后模拟 UnitOfWork 并将其与已模拟的 token 和 product 存储库进行关联。`_productService` 和 `_tokenService` 分别是 ProductService 和 TokenService 的实例,都使用模拟的 Unit of Work 进行初始化。
以下是我希望进一步解释的代码行:
var tokenEntity = _tokenService.GenerateToken(1);
_token = tokenEntity.AuthToken;
_client.DefaultRequestHeaders.Add("Token", _token);
在上面的代码中,我们使用令牌在请求头中初始化 `_client`,即 HttpClient。我们这样做是因为,如果您还记得我们在 Product Controller 中实现的安全性(身份验证和授权),它规定除非请求经过授权(即在其头中包含身份验证令牌),否则不接受任何请求。因此,我们在此通过 TokenService 的 `GenerateToken()` 方法生成令牌,传递默认用户 ID“1”,并使用该令牌进行授权。我们仅需要此设置来进行集成测试,因为单元测试将直接从我们的单元测试方法调用控制器方法,但对于实际的集成测试,您必须在调用 API 端点之前模拟所有先决条件。
SetUpProducts()
private static List<Product> SetUpProducts()
{
var prodId = new int();
var products = DataInitializer.GetAllProducts();
foreach (Product prod in products)
prod.ProductId = ++prodId;
return products;
}
SetUpTokens()
private static List<Token> SetUpTokens()
{
var tokId = new int();
var tokens = DataInitializer.GetAllTokens();
foreach (Token tok in tokens)
tok.TokenId = ++tokId;
return tokens;
}
编写测试夹具拆卸
与 [TestFixtureTearDown
] 不同,拆卸用于解除分配或处理对象。
以下是拆卸代码:
[TestFixtureTearDown]
public void DisposeAllObjects()
{
_tokenService = null;
_productService = null;
_unitOfWork = null;
_tokenRepository = null;
_productRepository = null;
_tokens = null;
_products = null;
if (_response != null)
_response.Dispose();
if (_client != null)
_client.Dispose();
}
编写测试设置
在这种情况下,只有在编写集成测试时才需要 Setup。因此,您可以选择省略它。
[SetUp]
public void ReInitializeTest()
{
_client = new HttpClient { BaseAddress = new Uri(ServiceBaseURL) };
_client.DefaultRequestHeaders.Add("Token", _token);
}
编写测试拆卸
测试 [TearDown
] 在每次测试执行完成后调用。
[TearDown]
public void DisposeTest()
{
if (_response != null)
_response.Dispose();
if (_client != null)
_client.Dispose();
}
模拟 Repository
我创建了一个名为 `SetUpProductRepository()` 的方法来模拟 Product Repository 并将其分配给 `ReInitializeTest()` 方法中的 `_productrepository`,以及 `SetUpTokenRepository()` 来模拟 TokenRepository 并将其分配给 `ReInitializeTest()` 方法中的 `_tokenRepository`。
SetUpProductRepository()
private GenericRepository<Product> SetUpProductRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Product>>(MockBehavior.Default, _dbEntities);
// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_products);
mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Product>(
id => _products.Find(p => p.ProductId.Equals(id))));
mockRepo.Setup(p => p.Insert((It.IsAny<Product>())))
.Callback(new Action<Product>(newProduct =>
{
dynamic maxProductID = _products.Last().ProductId;
dynamic nextProductID = maxProductID + 1;
newProduct.ProductId = nextProductID;
_products.Add(newProduct);
}));
mockRepo.Setup(p => p.Update(It.IsAny<Product>()))
.Callback(new Action<Product>(prod =>
{
var oldProduct = _products.Find(a => a.ProductId == prod.ProductId);
oldProduct = prod;
}));
mockRepo.Setup(p => p.Delete(It.IsAny<Product>()))
.Callback(new Action<Product>(prod =>
{
var productToRemove =
_products.Find(a => a.ProductId == prod.ProductId);
if (productToRemove != null)
_products.Remove(productToRemove);
}));
// Return mock implementation object
return mockRepo.Object;
}
SetUpTokenRepository()
private GenericRepository<Token> SetUpTokenRepository()
{
// Initialise repository
var mockRepo = new Mock<GenericRepository<Token>>(MockBehavior.Default, _dbEntities);
// Setup mocking behavior
mockRepo.Setup(p => p.GetAll()).Returns(_tokens);
mockRepo.Setup(p => p.GetByID(It.IsAny<int>()))
.Returns(new Func<int, Token>(
id => _tokens.Find(p => p.TokenId.Equals(id))));
mockRepo.Setup(p => p.Insert((It.IsAny<Token>())))
.Callback(new Action<Token>(newToken =>
{
dynamic maxTokenID = _tokens.Last().TokenId;
dynamic nextTokenID = maxTokenID + 1;
newToken.TokenId = nextTokenID;
_tokens.Add(newToken);
}));
mockRepo.Setup(p => p.Update(It.IsAny<Token>()))
.Callback(new Action<Token>(token =>
{
var oldToken = _tokens.Find(a => a.TokenId == token.TokenId);
oldToken = token;
}));
mockRepo.Setup(p => p.Delete(It.IsAny<Token>()))
.Callback(new Action<Token>(prod =>
{
var tokenToRemove =
_tokens.Find(a => a.TokenId == prod.TokenId);
if (tokenToRemove != null)
_tokens.Remove(tokenToRemove);
}));
// Return mock implementation object
return mockRepo.Object;
}
单元测试
一切就绪,我们可以开始为 ProductController 编写单元测试了。我们将编写测试以执行 ProductController 中的所有 CRUD 操作以及所有操作出口点。
1. GetAllProductsTest ()
我们的 BusinessServices 项目中的 ProductService 包含一个名为 `GetAllProducts()` 的方法,以下是实现:
[Test]
public void GetAllProductsTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/all")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
_response = productController.Get();
var responseResult = JsonConvert.DeserializeObject<List<Product>>(_response.Content.ReadAsStringAsync().Result);
Assert.AreEqual(_response.StatusCode, HttpStatusCode.OK);
Assert.AreEqual(responseResult.Any(), true);
var comparer = new ProductComparer();
CollectionAssert.AreEqual(
responseResult.OrderBy(product => product, comparer),
_products.OrderBy(product => product, comparer), comparer);
}
我将一步一步地解释代码。我们首先创建一个 ProductController 实例,并使用新的请求消息初始化控制器的 Request 属性,该消息指示调用 http 方法为 GET,并将 `RequestUri` 初始化为托管服务的基本 URL,并附加方法的实际端点。在这种情况下,初始化 `RequestUri` 不是必需的,但如果您测试实际服务端点,它将会有帮助。在本例中,我们不是测试实际端点,而是直接测试控制器方法。
`HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration()` 行将默认的 `httpconfiguration` 添加到控制器实例实例化所需的 `HttpConfigurationKey`。
`_response = productController.Get();` 行调用控制器的方法 `Get()`,该方法从模拟的 `_products` 列表中获取所有产品。由于该方法的返回类型是 http 响应消息,因此我们需要解析它以获取方法发送的 JSON 结果。API 中的所有事务理想情况下都应仅以 JSON 或 XML 形式进行。这有助于客户端理解响应及其结果集。我们使用 NewtonSoft 库将从 _response 获得的 JSON 对象反序列化为产品列表。这意味着 JSON 响应被转换为 List
`Assert.AreEqual(_response.StatusCode, HttpStatusCode.OK);` 行检查响应的 http 状态码,预期值为 `HttpStatusCode.OK`。
第二个断言,即 `Assert.AreEqual(responseResult.Any(), true);` 检查列表中是否包含项。第三个断言是我们测试的实际确认断言,它将实际产品列表中的每个产品与返回的产品列表进行比较。
我们获得了两个列表,并且需要检查列表的比较。我只需按 F5,然后在 TestUI 上获得结果:
这表明我们的测试已通过,即预期结果与返回结果相同。
2. GetProductByIdTest ()
此单元测试验证如果我们尝试调用 Product Controller 的 `GetProductById()` 方法,是否会返回正确的结果。
[Test]
public void GetProductByIdTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/productid/2")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
_response = productController.Get(2);
var responseResult = JsonConvert.DeserializeObject<Product>(_response.Content.ReadAsStringAsync().Result);
Assert.AreEqual(_response.StatusCode, HttpStatusCode.OK);
AssertObjects.PropertyValuesAreEquals(responseResult,
_products.Find(a => a.ProductName.Contains("Mobile")));
}
我使用了一个示例产品 ID“2”来测试该方法。同样,我们在 `HttpResponse` 中以 JSON 格式获取结果,并对其进行反序列化。第一个断言检查状态码,第二个断言使用 `AssertObject` 类来比较返回的产品属性与产品列表中的实际名为“mobile”且产品 ID 为 2 的产品。
测试 WebAPI 中的异常
NUnit 提供了测试异常的灵活性。现在,如果我们想对 `GetProductById()` 方法的备用出口点,即异常,进行单元测试,我们该怎么做?请记住,测试业务服务方法的备用出口点很容易,因为它返回 null。现在,在异常情况下,NUnit 提供了 ExpectedException 属性。我们可以定义从方法调用预期的异常类型。例如,如果我们使用错误的 ID 调用相同的方法,预期是它应该返回一个 ErrorCode 为 1001 的异常,并附带一个错误描述,说明“找不到该 ID 的产品”。
因此,在我们的例子中,预期的异常类型是 `ApiDataException`(从控制器方法中获取)。因此,我们可以将 Exception 属性定义为 [ExpectedException("WebApi.ErrorHelper.ApiDataException")
]。
然后使用错误的 ID 调用控制器方法。但是,还有一种断言异常的方法。NUnit 还灵活地通过 `Assert.Throws` 断言异常。此语句断言异常并将该特定异常返回给调用者。一旦我们获得该特定异常,我们就可以对其 `ErrorCode` 和 `ErrorDescription` 或任何您想断言的属性进行断言。
3. GetProductByWrongIdTest ()
[Test]
//[ExpectedException("WebApi.ErrorHelper.ApiDataException")]
public void GetProductByWrongIdTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/productid/10")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
var ex = Assert.Throws<ApiDataException>(() => productController.Get(10));
Assert.That(ex.ErrorCode,Is.EqualTo(1001));
Assert.That(ex.ErrorDescription, Is.EqualTo("No product found for this id."));
}
在上面的代码中,我注释掉了 `Exception` 属性方法,并采用了替代方法。
我在语句中使用了不存在于我们的产品列表中的错误 ID 调用了该方法:
var ex = Assert.Throws<ApiDataException>(() => productController.Get(10));
上述语句期望 `ApiDataException` 并将返回的异常存储在“ex”中。
现在我们可以断言“ex”异常的属性,如 `ErrorCode` 和 `ErrorDescription`,以及实际期望的结果。
4. GetProductByInvalidIdTest ()
同一方法的另一个出口点是,如果请求的产品 ID 无效,则会抛出异常。让我们对此场景测试该方法:
[Test]
// [ExpectedException("WebApi.ErrorHelper.ApiException")]
public void GetProductByInvalidIdTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/productid/-1")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
var ex = Assert.Throws<ApiException>(() => productController.Get(-1));
Assert.That(ex.ErrorCode, Is.EqualTo((int)HttpStatusCode.BadRequest));
Assert.That(ex.ErrorDescription, Is.EqualTo("Bad Request..."));
}
我向控制器方法传递了一个无效 ID,即 -1,它会抛出一个 `ApiException` 类型的异常,其 `ErrorCode` 等于 `HttpStatusCode.BadRequest`,`ErrorDescription` 等于“bad Request…”。
测试结果
即 Passed。其他测试与我解释的非常相似。
5. CreateProductTest ()
/// <summary>
/// Create product test
/// </summary>
[Test]
public void CreateProductTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Post,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/Create")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
var newProduct = new ProductEntity()
{
ProductName = "Android Phone"
};
var maxProductIDBeforeAdd = _products.Max(a => a.ProductId);
newProduct.ProductId = maxProductIDBeforeAdd + 1;
productController.Post(newProduct);
var addedproduct = new Product() { ProductName = newProduct.ProductName, ProductId = newProduct.ProductId };
AssertObjects.PropertyValuesAreEquals(addedproduct, _products.Last());
Assert.That(maxProductIDBeforeAdd + 1, Is.EqualTo(_products.Last().ProductId));
}
6. UpdateProductTest ()
/// <summary>
/// Update product test
/// </summary>
[Test]
public void UpdateProductTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Put,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/Modify")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
var firstProduct = _products.First();
firstProduct.ProductName = "Laptop updated";
var updatedProduct = new ProductEntity() { ProductName = firstProduct.ProductName, ProductId = firstProduct.ProductId };
productController.Put(firstProduct.ProductId, updatedProduct);
Assert.That(firstProduct.ProductId, Is.EqualTo(1)); // hasn't changed
}
7. DeleteProductTest ()
/// <summary>
/// Delete product test
/// </summary>
[Test]
public void DeleteProductTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Put,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/Remove")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
int maxID = _products.Max(a => a.ProductId); // Before removal
var lastProduct = _products.Last();
// Remove last Product
productController.Delete(lastProduct.ProductId);
Assert.That(maxID, Is.GreaterThan(_products.Max(a => a.ProductId))); // Max id reduced by 1
}
8. DeleteInvalidProductTest ()
/// <summary>
/// Delete product test with invalid id
/// </summary>
[Test]
public void DeleteProductInvalidIdTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Put,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/remove")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
var ex = Assert.Throws<ApiException>(() => productController.Delete(-1));
Assert.That(ex.ErrorCode, Is.EqualTo((int)HttpStatusCode.BadRequest));
Assert.That(ex.ErrorDescription, Is.EqualTo("Bad Request..."));
}
9. DeleteProductWithWrongIdTest ()
/// <summary>
/// Delete product test with wrong id
/// </summary>
[Test]
public void DeleteProductWrongIdTest()
{
var productController = new ProductController(_productService)
{
Request = new HttpRequestMessage
{
Method = HttpMethod.Put,
RequestUri = new Uri(ServiceBaseURL + "v1/Products/Product/remove")
}
};
productController.Request.Properties.Add(HttpPropertyKeys.HttpConfigurationKey, new HttpConfiguration());
int maxID = _products.Max(a => a.ProductId); // Before removal
var ex = Assert.Throws<ApiDataException>(() => productController.Delete(maxID+1));
Assert.That(ex.ErrorCode, Is.EqualTo(1002));
Assert.That(ex.ErrorDescription, Is.EqualTo("Product is already deleted or not exist in system."));
}
以上所有测试都可以自行解释,并且与我们测试 BusinessServices 的方式非常相似。目的是解释如何在 WebAPI 中编写测试。让我们通过 NUnit UI 运行所有测试。
通过 NUnit UI 进行测试

- 步骤 1
启动 NUnit UI。我已解释如何在 Windows 计算机上安装 NUnit。只需通过其启动图标启动 NUnit 界面,
- 第二步
界面打开后,单击 File -> New Project,将项目命名为 WebAPI.nunit,并将其保存在任何 Windows 位置。
- 步骤 3:然后,单击 Project -> Add Assembly,并浏览到 *ApiController.Tests.dll*(编译您的单元测试项目时创建的库)。
- 步骤 4:浏览程序集后,您将看到该测试项目的所有单元测试都已加载到 UI 中并在界面上可见。
- 步骤 5:在界面的右侧面板中,您会看到一个 Run 按钮,用于运行 Api 控制器的所有测试。只需在左侧的测试树中选择 ApiController 节点,然后在右侧按下 Run 按钮。
运行测试后,您将在右侧看到绿色的进度条,并在左侧的所有测试上看到勾号。这意味着所有测试都已通过。如果任何测试失败,您将在测试上看到一个叉号,并在右侧看到一个红色的进度条。
但是,我们所有的测试都通过了。
集成测试
我将简单介绍一下集成测试是什么以及如何编写。集成测试不在内存中运行。对于 WebAPI,编写集成测试的最佳实践是当 WebAPI 自托管时。您可以尝试在托管 API 时编写集成测试,这样您就可以获得要测试的服务。的实际 URL 或端点。该测试在实际数据和实际服务上执行。让我们举个例子。我已托管我的 Web API,并且我想测试 WebAPI 的 `GetAllProducts()` 方法。该特定控制器操作的托管 URL 为 https://:50875/v1/Products/Product/allproducts。
现在我知道我不会通过 DLL 引用来测试我的控制器方法,而是想实际测试它的端点,为此我需要提供一个身份验证令牌,因为该端点是受保护的,除非我在请求头中添加一个安全令牌,否则无法授权。以下是 `GetAllProducts()` 的集成测试。
[Test]
public void GetAllProductsIntegrationTest()
{
#region To be written inside Setup method specifically for integration tests
var client = new HttpClient { BaseAddress = new Uri(ServiceBaseURL) };
client.DefaultRequestHeaders.Add("Authorization", "Basic YWtoaWw6YWtoaWw=");
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
_response = client.PostAsync("login", null).Result;
if (_response != null && _response.Headers != null && _response.Headers.Contains("Token") && _response.Headers.GetValues("Token") != null)
{
client.DefaultRequestHeaders.Clear();
_token = ((string[])(_response.Headers.GetValues("Token")))[0];
client.DefaultRequestHeaders.Add("Token", _token);
}
#endregion
_response = client.GetAsync("v1/Products/Product/allproducts/").Result;
var responseResult =
JsonConvert.DeserializeObject<List<ProductEntity>>(_response.Content.ReadAsStringAsync().Result);
Assert.AreEqual(_response.StatusCode, HttpStatusCode.OK);
Assert.AreEqual(responseResult.Any(), true);
}
我使用相同的类来编写此测试,但您应该始终将单元测试与集成测试分开,因此请使用另一个测试项目来编写 Web API 的集成测试。首先,我们需要请求一个令牌并将其添加到客户端请求中:
var client = new HttpClient { BaseAddress = new Uri(ServiceBaseURL) };
client.DefaultRequestHeaders.Add("Authorization", "Basic YWtoaWw6YWtoaWw=");
MediaTypeFormatter jsonFormatter = new JsonMediaTypeFormatter();
_response = client.PostAsync("login", null).Result;
if (_response != null && _response.Headers != null && _response.Headers.Contains("Token") && _response.Headers.GetValues("Token") != null)
{
client.DefaultRequestHeaders.Clear();
_token = ((string[])(_response.Headers.GetValues("Token")))[0];
client.DefaultRequestHeaders.Add("Token", _token);
}
在上面的代码中,我使用运行服务的基本 URL,即 https://:50875 来初始化客户端。初始化后,我设置了一个默认请求头来调用我的身份验证控制器的登录端点以获取有效的令牌。用户用其凭据登录后,他将获得一个有效的令牌。有关安全性的详细信息,请参阅我关于Web API 安全性的文章。我传递了我的凭据用户名:Akhil 和密码:Akhil 的 base64 字符串进行基本身份验证。请求通过身份验证后,我将在 _response.Headers 中获得一个有效的令牌,我将其获取并分配给 `_token` 变量,并使用此代码行将其添加到客户端的默认头中:
_token = ((string[])(_response.Headers.GetValues("Token")))[0];
client.DefaultRequestHeaders.Add("Token", _token);
然后我从同一个客户端调用实际的服务 URL:
_response = client.GetAsync("v1/Products/Product/allproducts/").Result;
我们得到了成功的结果。请参见屏幕截图。
步骤 1:获取令牌
我们获得了令牌:4bffc06f-d8b1-4eda-b4e6-df9568dd53b1。 现在,因为这是一个实时测试。该令牌应保存在数据库中。让我们检查一下。
步骤 2:检查数据库
我们在数据库中找到了相同的令牌。这证明我们正在真实有效的 URL 上进行测试。
步骤 3:检查 ResponseResult
在这里,我们得到了结果,其中包含 6 个产品,第一个产品的 ID 是 1,产品名称是“Laptop”。请查看数据库以获取完整的产品列表。
我们得到了相同的数据。这证明我们的测试是成功的。
同样,您可以编写更多集成测试。
单元测试与集成测试的区别
我不会写太多,但想从这个参考链接分享我对此的一些不错的阅读。
单元测试 | 集成测试 |
单元测试是一种测试,用于检查一小段代码是否按预期工作。 | 集成测试是一种测试,用于检查模块的不同部分是否协同工作。 |
单元测试检查应用程序的单个组件。 | 集成测试考虑了集成模块的行为。 |
单元测试的范围很窄,它涵盖了被测单元或一小段代码。因此,在编写单元测试时,会使用较短的代码,这些代码仅针对单个类。 | 集成测试的范围很广,它涵盖了被测应用程序的整体,并且需要投入更多精力来组织。 |
单元测试不应依赖于被测单元之外的代码。 | 集成测试依赖于其他外部系统,如数据库、为其分配的硬件等。 |
这是软件测试生命周期中要进行的第一个测试类型,通常由开发人员执行。 | 此测试在单元测试之后、系统测试之前进行,并由测试团队执行。 |
单元测试不进一步细分。 | 集成测试进一步细分为以下几种类型: |
自顶向下集成、自底向上集成等。 | |
单元测试从模块规范开始。 | 集成测试从接口规范开始。 |
代码的详细可见性属于单元测试。 | 集成结构的可见性属于集成测试。 |
单元测试主要关注测试单个单元的功能,而无法发现不同模块交互时出现的问题。 | 集成测试是为了发现不同模块交互以构建整体系统时出现的问题。 |
单元测试的目的是单独测试每个单元,并确保每个单元都按预期工作。 | 集成测试的目的是组合测试模块,并确保每个组合模块都按预期工作。 |
单元测试属于白盒测试类型。 | 集成测试属于黑盒和白盒测试类型。 |
结论
在本文中,我们学习了如何为 Web API 控制器编写单元测试,主要关注基本的 CRUD 操作。目的是让您对如何编写和执行单元测试有一个基本的了解。您可以添加自己的风格,这有助于您在实际项目中。我们还学习了如何为 WebAPI 端点编写集成测试。我希望这对您有所帮助。您可以从GitHub 下载本文的完整源代码(包含程序包)。编码愉快 :)
其他系列
我的其他系列文章
- MVC 架构和关注点分离简介:第一部分
- 深入了解 OOP(第 1 天):多态和继承(早期绑定/编译时多态)