【问题标题】:How to write Functional Tests against ServiceStack API如何针对 ServiceStack API 编写功能测试
【发布时间】:2013-10-05 23:23:15
【问题描述】:

我们有一个与 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?我只是像单元测试一样调用方法吗?我想这是我无法理解的“功能测试”部分。

【问题讨论】:

    标签: c# api nunit servicestack functional-testing


    【解决方案1】:

    测试服务合同

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

    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 才能让服务成功打开该端口;请参阅下面的 cmets 和 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 针对进程内主机运行测试,上述实现基于该主机。

    【讨论】:

    • 很好的答案!除了 JSON Schema 之外,替代模式验证选项可能包括维护一个集成测试套件 Serialize/Deserialize 基于使用 已发布的 DTO 的请求(确保更改不会破坏现有的 .NET 客户端,这也是一个很好的指示'不破坏限制较少的动态客户端),以及使用/metadata?xsd=1 上生成的 XSD 架构中的类型来验证 XML 输出。
    • 这是一个很棒的答案。我明天要开始了……非常感谢!
    • “您的业务逻辑和数据访问没有在您的 ServiceStack 服务类中实现,对吗?” 实际上,我们的业务逻辑完全包含在核心 PCL 中。在那里,我们有完全 集成 测试的存储库,并为我们的服务完全模拟了单元测试。 ServiceStack Server 项目只包含裸 api 代码,并请求这样的服务...var response = new UserResponse { User = _userService.Save( request ) };
    • @ChaseFlorell 很高兴听到服务层与逻辑/数据访问及其测试的分离。所以功能测试实际上都是为了确保 Web 服务接口本身是正确的。因此,我的建议是让功能测试专注于特定的用例,不要为 100% 的逻辑覆盖率而烦恼。
    • 一个地狱般的答案!
    猜你喜欢
    • 1970-01-01
    • 2013-08-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多