如何针对ServiceStack API编写功能测试

Cha*_*ell 10 c# api nunit functional-testing servicestack

我们有一个使用ServiceStack连接的ASP.NET Web应用程序.我之前从未编写过功能测试,但我们的任务是针对我们的API编写测试(nUnit)并证明它一直工作到数据库级别.

有人可以帮我开始编写这些测试吗?

以下是post我们的用户服务的方法示例.

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 () ) } }
    };
}
Run Code Online (Sandbox Code Playgroud)

现在我知道如何编写一个标准的单元测试,但我对此部分感到困惑.我是否必须通过HTTP调用WebAPI并初始化Post?我是否只是像单位测试那样调用方法?我想这是"功能测试"的一部分让我望而却步.

Mik*_*ock 18

测试服务合同

对于端到端功能测试,我专注于验证服务是否可以接受请求消息并为简单的用例生成预期的响应消息.

Web服务是一种合同:给定某种形式的消息,该服务将产生给定表单的响应消息.第二,该服务将以某种方式改变其底层系统的状态.请注意,对于最终客户端,消息不是您的DTO类,而是给定文本格式(JSON,XML等)的请求的特定示例,使用特定动词发送到特定URL,具有给定集合标题.

ServiceStack Web服务有多个层:

client -> message -> web server -> ServiceStack host -> service class -> business logic
Run Code Online (Sandbox Code Playgroud)

简单的单元测试和集成测试最适合业务逻辑层.通常很容易直接针对您的服务类编写单元测试:构建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();
    }
}
Run Code Online (Sandbox Code Playgroud)

您的实际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);
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,您应该为所有测试运行进程内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
}
Run Code Online (Sandbox Code Playgroud)

请注意,您可能必须以管理员模式运行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);
}
Run Code Online (Sandbox Code Playgroud)

要验证响应,请创建一个JSON模式文件,该文件描述了响应的预期格式:此特定用例需要存在哪些字段,需要哪些数据类型等.此实现使用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));
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个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}
        }
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

这种类型的测试应该写一次并且永远不会被修改,除非它的模型用例变得过时.我们的想法是,这些测试将代表生产中API的实际用法,并确保API承诺返回的确切消息不会以破坏现有用法的方式发生变化.

其他信息

ServiceStack本身有一些针对进程内主机运行测试的示例,上面的实现基于该主机.