测试服务合同
对于端到端的功能测试,我专注于验证服务是否可以接受请求消息并为简单的用例生成预期的响应消息。
Web 服务是一个契约:给定一个特定形式的消息,该服务将产生一个给定形式的响应消息。其次,服务将以某种方式改变其底层系统的状态。请注意,对于最终客户端,消息不是您的 DTO 类,而是给定文本格式(JSON、XML 等)的请求的特定示例,使用特定动词发送到特定 URL,具有给定集合的标题。
ServiceStack Web 服务有多个层次:
client -> message -> web server -> ServiceStack host -> service class -> business logic
简单的单元测试和集成测试最适合业务逻辑层。直接针对您的服务类编写单元测试通常也很容易:构造一个 DTO 对象、在您的服务类上调用 Get/Post 方法以及验证响应对象应该很容易。但是这些不会测试 ServiceStack 主机内部发生的任何事情:路由、序列化/反序列化、请求过滤器的执行等。当然,您不想测试 ServiceStack 代码本身,因为它是具有自己的单元测试的框架代码. 但是有机会测试特定请求消息进入服务和从服务中出来的特定路径。这是服务契约中不能通过直接查看服务类来完全验证的部分。
不要尝试 100% 的覆盖率
我不建议尝试通过这些功能测试来 100% 覆盖所有业务逻辑。我专注于用这些测试覆盖主要用例——每个端点通常有一个或两个请求示例。通过针对您的业务逻辑类编写传统的单元测试,可以更有效地完成对特定业务逻辑案例的详细测试。(您的业务逻辑和数据访问没有在您的 ServiceStack 服务类中实现,对吧?)
实施
我们将在进程内运行 ServiceStack 服务并使用 HTTP 客户端向它发送请求,然后验证响应的内容。这个实现是特定于 NUnit 的;在其他框架中应该可以实现类似的实现。
首先,您需要一个在所有测试之前运行的 NUnit 设置夹具,以设置进程内 ServiceStack 主机:
// this needs to be in the root namespace of your functional tests
public class ServiceStackTestHostContext
{
[TestFixtureSetUp] // this method will run once before all other unit tests
public void OnTestFixtureSetUp()
{
AppHost = new ServiceTestAppHost();
AppHost.Init();
AppHost.Start(ServiceTestAppHost.BaseUrl);
// do any other setup. I have some code here to initialize a database context, etc.
}
[TestFixtureTearDown] // runs once after all other unit tests
public void OnTestFixtureTearDown()
{
AppHost.Dispose();
}
}
您实际的 ServiceStack 实现可能有一个AppHost
类,它是AppHostBase
(至少如果它在 IIS 中运行)的子类。我们需要继承一个不同的基类来在进程中运行这个 ServiceStack 主机:
// the main detail is that this uses a different base class
public class ServiceTestAppHost : AppHostHttpListenerBase
{
public const string BaseUrl = "http://localhost:8082/";
public override void Configure(Container container)
{
// Add some request/response filters to set up the correct database
// connection for the integration test database (may not be necessary
// depending on your implementation)
RequestFilters.Add((httpRequest, httpResponse, requestDto) =>
{
var dbContext = MakeSomeDatabaseContext();
httpRequest.Items["DatabaseIntegrationTestContext"] = dbContext;
});
ResponseFilters.Add((httpRequest, httpResponse, responseDto) =>
{
var dbContext = httpRequest.Items["DatabaseIntegrationTestContext"] as DbContext;
if (dbContext != null) {
dbContext.Dispose();
httpRequest.Items.Remove("DatabaseIntegrationTestContext");
}
});
// now include any configuration you want to share between this
// and your regular AppHost, e.g. IoC setup, EndpointHostConfig,
// JsConfig setup, adding Plugins, etc.
SharedAppHost.Configure(container);
}
}
现在,您应该为所有测试运行一个进程内 ServiceStack 服务。现在向该服务发送请求非常简单:
[Test]
public void MyTest()
{
// first do any necessary database setup. Or you could have a
// test be a whole end-to-end use case where you do Post/Put
// requests to create a resource, Get requests to query the
// resource, and Delete request to delete it.
// I use RestSharp as a way to test the request/response
// a little more independently from the ServiceStack framework.
// Alternatively you could a ServiceStack client like JsonServiceClient.
var client = new RestClient(ServiceTestAppHost.BaseUrl);
client.Authenticator = new HttpBasicAuthenticator(NUnitTestLoginName, NUnitTestLoginPassword);
var request = new RestRequest...
var response = client.Execute<ResponseClass>(request);
// do assertions on the response object now
}
请注意,您可能必须在管理员模式下运行 Visual Studio 才能使服务成功打开该端口;请参阅下面的评论和这个后续问题。
更进一步:模式验证
我为企业系统开发 API,客户为定制解决方案支付大量资金并期望获得高度稳健的服务。因此,我们使用模式验证来绝对确保我们不会在最低级别破坏服务合同。我认为大多数项目都不需要模式验证,但如果您想进一步测试,可以执行以下操作。
您可能无意中破坏服务合同的一种方法是以不向后兼容的方式更改 DTO:例如,重命名现有属性或更改自定义序列化代码。这可能会通过使数据不再可用或无法解析来破坏您的服务客户端,但您通常无法通过对业务逻辑进行单元测试来检测此更改。防止这种情况发生的最好方法是让您的请求 DTO 保持独立和单一用途,并与您的业务/数据访问层分开,但仍有可能有人会不小心错误地应用重构。
为了防止这种情况,您可以在功能测试中添加模式验证。我们仅针对我们知道付费客户实际上将在生产中使用的特定用例执行此操作。这个想法是,如果这个测试失败,那么我们知道如果要部署到生产中,破坏测试的代码会破坏这个客户端的集成。
[Test(Description = "Ticket # where you implemented the use case the client is paying for")]
public void MySchemaValidationTest()
{
// Send a raw request with a hard-coded URL and request body.
// Use a non-ServiceStack client for this.
var request = new RestRequest("/service/endpoint/url", Method.POST);
request.RequestFormat = DataFormat.Json;
request.AddBody(requestBodyObject);
var response = Client.Execute(request);
Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
RestSchemaValidator.ValidateResponse("ExpectedResponse.json", response.Content);
}
要验证响应,请创建一个JSON Schema文件,该文件描述响应的预期格式:此特定用例需要存在哪些字段、预期的数据类型等。此实现使用Json.NET 模式解析器。
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Schema;
public static class RestSchemaValidator
{
static readonly string ResourceLocation = typeof(RestSchemaValidator).Namespace;
public static void ValidateResponse(string resourceFileName, string restResponseContent)
{
var resourceFullName = "{0}.{1}".FormatUsing(ResourceLocation, resourceFileName);
JsonSchema schema;
// the json file name that is given to this method is stored as a
// resource file inside the test project (BuildAction = Embedded Resource)
using(var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceFullName))
using(var reader = new StreamReader(stream))
using (Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceFileName))
{
var schematext = reader.ReadToEnd();
schema = JsonSchema.Parse(schematext);
}
var parsedResponse = JObject.Parse(restResponseContent);
Assert.DoesNotThrow(() => parsedResponse.Validate(schema));
}
}
这是一个 json 模式文件的示例。请注意,这是特定于这一用例的,不是响应 DTO 类的通用描述。这些属性都标记为必需,因为这些是客户在此用例中期望的特定属性。架构可能会忽略响应 DTO 中当前存在的其他未使用的属性。基于此架构,RestSchemaValidator.ValidateResponse
如果响应 JSON 中缺少任何预期字段、具有意外数据类型等,调用将失败。
{
"description": "Description of the use case",
"type": "object",
"additionalProperties": false,
"properties":
{
"SomeIntegerField": {"type": "integer", "required": true},
"SomeArrayField": {
"type": "array",
"required": true,
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"Property1": {"type": "integer", "required": true},
"Property2": {"type": "string", "required": true}
}
}
}
}
}
这种类型的测试应该编写一次并且永远不要修改,除非它所建模的用例已经过时。这个想法是,这些测试将代表您的 API 在生产中的实际使用情况,并确保您的 API 承诺返回的确切消息不会以破坏现有使用的方式发生变化。
其他信息
ServiceStack 本身有一些针对进程内主机运行测试的示例,上述实现基于这些示例。