有人可以帮我开始写这些测试吗?
public object Post( UserRequest request ) { var response = new UserResponse { User = _userService.Save( request ) }; return new HttpResult( response ) { StatusCode = HttpStatusCode.Created,Headers = { { HttpHeaders.Location,base.Request.AbsoluteUri.CombineWith( response.User.Id.ToString () ) } } }; }
现在我知道如何写一个标准的单元测试,但在这一部分我感到困惑.我必须通过HTTP调用WebAPI并初始化一篇文章?我只是像我单位测试一样调用方法吗?我认为这是“功能测试”的一部分,不包括我.
解决方法
对于端到端功能测试,我着重于验证服务是否可以接受请求消息,并为简单的用例产生预期的响应消息.
Web服务是一种合同:给定某种形式的消息,该服务将产生给定形式的响应消息.第二,服务将以某种方式改变其基础系统的状态.请注意,对于最终客户端,消息不是您的DTO类,而是以特定动词发送给特定URL的给定文本格式(JSON,XML等)的请求的特定示例,其中给定集合的标题.
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,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才能使服务成功打开该端口;见下面的评论和this follow-up question.
进一步:模式验证
我在一个企业系统的API上工作,在那里客户为自定义解决方案付出了大量的代价,并期待着一个非常强大的服务.因此,我们使用模式验证来绝对确定我们不会在最低级别打破服务合同.我不认为模式验证对于大多数项目是必要的,但是如果您想进一步测试一下,您可以执行以下操作.
您可以无意中破坏您的服务合同的方式之一是以不向后兼容的方式更改DTO:例如,重命名现有属性或更改自定义序列化代码.这可以通过使数据不再可用或可解析来破坏您的服务的客户端,但您通常无法通过对业务逻辑进行单元测试来检测此更改.防止这种情况发生的最好方法是keep your request DTOs separate and single-purpose and separate from your business/data access layer,但是有一个机会有人会意外地错误地应用重构.
为了防范这种情况,您可以在功能测试中添加模式验证.我们只针对具体的使用案例,我们知道付费客户端实际上将用于生产.这个想法是,如果这个测试中断了,那么我们知道打破测试的代码如果要部署到生产中,就会打破这个客户端的集成.
[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 schema parser.
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中当前存在的其他未使用的属性.基于此模式,如果响应JSON中缺少任何预期字段,具有意外数据类型等,则对RestSchemaValidator.ValidateResponse的调用将失败.
{ "description": "Description of the use case","type": "object","additionalProperties": false,"properties": { "SomeIntegerField": {"type": "integer","required": true},"SomeArrayField": { "type": "array","required": true,"items": { "type": "object","properties": { "Property1": {"type": "integer","Property2": {"type": "string","required": true} } } } } }
这种类型的测试应该写一次,除非被建模的用例变得过时,否则永远不会被修改.这个想法是这些测试将代表您在生产中的API的实际使用,并确保您的API承诺返回的确切消息不会以破坏现有用途的方式更改.
其他信息
ServiceStack本身有一些针对进程中主机进行测试的examples,上述实现基于此.