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

时间:2013-10-05 23:23:16

标签: c# api nunit servicestack functional-testing

我们有一个使用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 () ) } }
    };
}

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

1 个答案:

答案 0 :(得分:18)

测试服务合同

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

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

ServiceStack Web服务有多个层:

client -> message -> web server -> ServiceStack host -> service class -> business logic

简单的单元测试和集成测试最适合业务逻辑层。它通常也可以直接针对您的服务类轻松编写单元测试:构建DTO对象,在服务类上调用Get / Post方法以及验证响应对象应该很容易。但这些不会测试ServiceStack主机内发生的任何事情:路由,序列化/反序列化,请求过滤器的执行等。当然,您不希望自己测试ServiceStack代码本身#39 ; s框架代码,有自己的单元测试。但是,有机会测试特定请求消息进入服务并从中发出的特定路径。这是服务合同的一部分,无法通过直接查看服务类别来进行全面验证。

不要尝试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才能使服务成功打开该端口;请参阅下面的评论和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类的一般描述。属性 all 标记为必需,因为这些是客户在此用例中期望的特定属性。架构可能会遗漏响应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",
        "additionalProperties": false,
        "properties": {
          "Property1": {"type": "integer", "required": true},
          "Property2": {"type": "string", "required": true}
        }
      }
    }
  }
}

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

其他信息

ServiceStack本身对进程内主机进行了一些examples运行测试,上面的实现基于此进行。