jjzjj

c# - 如何根据ServiceStack API编写功能测试

coder 2024-06-01 原文

我们有一个连接到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?我是否像调用单元测试一样调用方法?我想是“功能测试”这一部分让我难以理解。

最佳答案

测试服务合同
对于端到端的功能测试,我关注的是验证服务可以接受请求消息,并为简单用例生成预期的响应消息。
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才能使服务成功打开该端口;请参阅下面的注释和this follow-up question
进一步:模式验证
我为一个企业系统开发了一个api,在这个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",
        "additionalProperties": false,
        "properties": {
          "Property1": {"type": "integer", "required": true},
          "Property2": {"type": "string", "required": true}
        }
      }
    }
  }
}

这种类型的测试应该只写一次,并且永远不要修改,除非它所建模的用例已经过时。其思想是,这些测试将代表api在生产中的实际用法,并确保api承诺返回的确切消息不会以破坏现有用法的方式发生更改。
其他信息
ServiceStack本身有一些针对进程内主机运行测试的examples,上面的实现是基于该主机的。

关于c# - 如何根据ServiceStack API编写功能测试,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/19203751/

有关c# - 如何根据ServiceStack API编写功能测试的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby-on-rails - 使用 Ruby on Rails 进行自动化测试 - 最佳实践 - 2

    很好奇,就使用ruby​​onrails自动化单元测试而言,你们正在做什么?您是否创建了一个脚本来在cron中运行rake作业并将结果邮寄给您?git中的预提交Hook?只是手动调用?我完全理解测试,但想知道在错误发生之前捕获错误的最佳实践是什么。让我们理所当然地认为测试本身是完美无缺的,并且可以正常工作。下一步是什么以确保他们在正确的时间将可能有害的结果传达给您? 最佳答案 不确定您到底想听什么,但是有几个级别的自动代码库控制:在处理某项功能时,您可以使用类似autotest的内容获得关于哪些有效,哪些无效的即时反馈。要确保您的提

  4. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  5. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  6. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  7. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  8. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

  9. ruby - 在 Ruby 中编写命令行实用程序 - 2

    我想用ruby​​编写一个小的命令行实用程序并将其作为gem分发。我知道安装后,Guard、Sass和Thor等某些gem可以从命令行自行运行。为了让gem像二进制文件一样可用,我需要在我的gemspec中指定什么。 最佳答案 Gem::Specification.newdo|s|...s.executable='name_of_executable'...endhttp://docs.rubygems.org/read/chapter/20 关于ruby-在Ruby中编写命令行实用程序

  10. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

随机推荐