REST保证:使用该工具五年来我们学到了什么

REST保证 -用于测试REST服务的DSL,已嵌入Java测试中。 该解决方案出现于九年前,并因其简单和方便的功能而变得流行。


在DINS中,我们使用它编写了17,000多个测试,并且在使用的五年中,我们遇到了许多“陷阱”,这些缺陷在将库导入项目后就无法发现:静态上下文,对查询应用过滤器的顺序混乱,难以构建测试。


本文介绍了REST保证的此类隐式功能。 如果项目中的测试数量有可能迅速增加,则需要考虑这些因素-这样您就不必在以后重写它们。


图片


我们正在测试什么


DINS参与了UCaaS平台的开发。 特别是,我们开发和测试RingCentral自身使用并提供给第三方开发人员的API。


开发任何API时,重要的是要确保其正常工作,但是当您发布它时,则必须检查更多情况。 因此,将数十个测试添加到每个新端点。 测试是用Java编写的,选择了TestNG作为测试框架,并使用REST Assured进行API请求。


REST获得保障将受益


如果您的目标不是彻底测试整个API,那么最简单的方法就是使用REST保证。 它非常适合检查响应结构,PVD和烟雾测试。


这是一个简单的测试的样子,它将在访问端点时检查端点的状态是否为200 OK:


given() .baseUri("http://cookiemonster.com") .when() .get("/cookies") .then() .assertThat() .statusCode(200); 

关键字givenwhen then构成请求的关键字: given确定了将在请求中发送的内容, when –使用哪种方法以及向哪个端点发送请求, then –如何检查收到的响应。 此外,您可以以JsonPathXmlPath类型的对象的形式提取响应主体,然后使用接收到的数据。


实际测试通常更大,更复杂。 标头,Cookie,授权,请求正文已添加到请求中。 而且,如果受测的API不包含数十种独特的资源,而每种资源都需要特殊的参数,则您将需要将现成的模板存储在某个地方,以便稍后将它们添加到测试中的特定调用中。


为此,在REST确保中有:


  • RequestSpecification / ResponseSpecification ;
  • 基本配置;
  • 过滤器。

请求规格和响应规格


这两个类使您可以从响应中确定请求参数和期望值:


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); requestSpec.when() .get("/cookiesformonster") .then() .statusCode(200); requestSpec.when() .get("/soup") .then() .statusCode(400); 

 ResponseSpecification responseSpec = expect() .statusCode(200); given() .expect() .spec(responseSpec) .when() .get("/hello"); given() .expect() .spec(responseSpec) .when() .get("/goodbye"); 

一个规范在多个调用,测试和测试类中使用,具体取决于定义的位置-没有限制。 您甚至可以将多个规范添加到单个请求中。 但是,这是潜在的问题根源


 RequestSpecification requestSpec = given() .baseUri("http://cookiemonster.com") .header("Language", "en"); RequestSpecification yetAnotherRequestSpec = given() .header("Language", "fr"); given() .spec(requestSpec) .spec(yetAnotherRequestSpec) .when() .get("/cookies") .then() .statusCode(200); 

通话记录:


 Request method: GET Request URI: http://localhost:8080/ Headers: Language=en Language=fr Accept=*/* Cookies: <none> Multiparts: <none> Body: <none> java.net.ConnectException: Connection refused (Connection refused) 

原来,所有标头都添加到了调用中,但是URI突然变成了localhost-尽管它是在第一个规范中添加的。


发生这种情况的原因是,REST保证人处理请求参数的替代方式有所不同(答案也是如此)。 标头或过滤器将添加到列表中,然后依次应用。 只能有一个URI,因此将应用最后一个。 在最后添加的规范中未指定它-因此,REST Assured将使用默认值(localhost)覆盖它。


如果在请求中添加了规范,请添加一个 。 该建议似乎很明显,但是当带有测试的项目增长时,将出现帮助程序类和基本测试类,而先于方法会出现在其中。 跟踪您的请求实际发生的情况变得很困难,尤其是当几个人一次编写测试时。


基本REST保证的配置


在REST Assured中使用模板查询的另一种方法是配置基本配置,并定义RestAssured类的静态字段:


 @BeforeMethod public void configureRestAssured(...) { RestAssured.baseURI = "http://cookiemonster.com"; RestAssured.requestSpecification = given() .header("Language", "en"); RestAssured.filters(new RequestLoggingFilter(), new ResponseLoggingFilter()); ... } 

每次都会将值自动添加到请求中。 该配置与TestNG中的@BeforeEach注释和JUnit中的@BeforeEach注释结合在一起,因此您可以确保运行的每个测试都将从相同的参数开始。


但是,该配置将是潜在的问题根源,因为它是静态的


示例:在每次测试之前,我们接受一个测试用户,为其获取授权令牌,然后通过AuthenticationScheme或授权过滤器将其添加到基本配置中。 只要测试在单个线程中运行,一切都会正常。
当测试过多时,通常的决定是将其执行划分为多个线程,这将导致重写一段代码,从而使一个线程中的令牌不会落入相邻的代码中。


REST保证的过滤器


过滤器会在发送之前修改请求,并在检查是否符合指定期望之前修改响应。 应用示例-添加日志记录或授权:


 public class OAuth2Filter implements AuthFilter { String accessToken; OAuth2Filter(String accessToken) { this.accessToken = accessToken; } @Override public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) { requestSpec.replaceHeader("Authorization", "Bearer " + accessToken); return ctx.next(requestSpec, responseSpec); } } 

 String accessToken = getAccessToken(username, password); OAuth2Filter auth = new OAuth2Filter(accessToken); given() .filter(auth) .filter(new RequestLoggingFilter()) .filter(new ResponseLoggingFilter()) ... 

添加到请求的过滤器存储在LinkedList 。 在发出请求之前,REST安全确保者通过遍历列表并依次应用一个过滤器来对其进行修改。 然后,对收到的答案也做同样的事情。


过滤器的顺序很重要 。 这两个查询将导致不同的日志:第一个查询将指示授权标头,第二个查询-否。 在这种情况下,标头将被添加到两个请求中-在第一种情况下,REST Assured将在注册之前首先添加授权,在第二种情况中-反之亦然。


 given() .filter(auth) .filter(new RequestLoggingFilter()) … given() .filter(new RequestLoggingFilter()) .filter(auth) 

除了通常按照过滤器添加的顺序应用过滤器的规则,还有机会通过实现OrderedFilter接口对过滤器进行优先级排序。 它允许您为过滤器设置特殊的数字优先级,该优先级高于或低于默认值(1000)。 具有较高优先级的过滤器将比平时更早执行,具有较低优先级的过滤器-在它们之后。


当然,在这里您可能会感到困惑,并意外地将两个过滤器设置为相同的优先级,例如在999。然后,之前添加的过滤器将首先应用于请求。


不仅过滤器


上面显示了如何通过过滤器进行授权。 但是除了REST Assured中的此方法外,还有另一种通过AuthenticationScheme


 String accessToken = getAccessToken(username, password); OAuth2Scheme scheme = new OAuth2Scheme(); scheme.setAccessToken(accessToken); RestAssured.authentication = scheme; 

这是一种过时的方法。 相反,您应该选择上面显示的那个。 有两个原因:


依赖问题


REST保证的文档表明 ,要使用Oauth1或Oauth2(通过将令牌指定为查询参数),必须根据抄写员添加授权。 但是,导入最新版本将无济于事-您将遇到其中一个未解决的问题中描述的错误。 您只能通过导入旧版本的库2.5.3来解决它。 但是,在这种情况下,您将遇到另一个问题


通常,没有其他版本的Scribe可与Oauth2 REST保证版本3.0.3及更高版本一起使用(并且最新版本4.0.0并未解决此问题)。


记录不起作用


过滤器以特定顺序应用于查询。 在它们之后应用AuthenticationScheme 。 这意味着很难在测试中检测到授权问题-未承诺。


有关REST保证语法的更多信息


大量测试通常意味着它们也很复杂。 并且如果API是测试的主要主题,并且您不仅需要检查json字段,还需要检查业务逻辑,那么使用REST保证,测试就变成了一张纸:


 @Test public void shouldCorrectlyCountAddedCookies() { Integer addNumber = 10; JsonPath beforeCookies = given() .when() .get("/latestcookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); String beforeId = beforeCookies.getString("id"); JsonPath afterCookies = given() .body(String.format("{number: %s}", addNumber)) .when() .put("/cookies") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); Integer afterNumber = afterCookies.getInt("number"); String afterId = afterCookies.getString("id"); JsonPath history = given() .when() .get("/history") .then() .assertThat() .statusCode(200) .extract() .jsonPath(); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", beforeId))) .isEqualTo(afterNumber - addNumber); assertThat(history.getInt(String.format("records.find{r -> r.id == %s}.number", afterId))) .isEqualTo(afterNumber); } 

该测试验证了当我们喂入一个怪物cookie时,我们正确地计算了给他的cookie数量,并在故事中指出了这一点。 但是乍看之下,这无法理解-所有请求看起来都一样,并且不清楚通过API进行数据准备的位置以及将测试请求发送到的位置。


given()when()以及when() REST Assured会从BDD中获取,例如Spock或Cucumber。 但是,在复杂的测试中,它们的含义会丢失,因为测试的范围变得比一个请求大得多-这是一个小动作,需要用一行来表示。 为此,您可以将REST保证的调用转移到辅助类:


 public class CookieMonsterHelper { public static JsonPath getCookies() { return given() .when() .get("/cookiesformonster") .then() .extract() .jsonPath(); } ... } 

并调用测试:


 JsonPath response = CookieMonsterHelper.getCookies(); 

如果此类帮助程序类具有通用性,那么可以将对一个方法的调用嵌入大量测试中,然后将它们放入一个单独的库中,这是很好的:通常,您突然需要在另一个项目中的某个位置调用该方法。 仅在这种情况下,您才需要删除所有“ Rest Assured”可以对响应进行的验证-毕竟,响应同一请求,通常会返回非常不同的数据。


结论


REST Assured是用于测试的库。 她知道如何做两件事:发送请求和检查答案。 如果我们尝试从测试中删除它并删除所有验证,那么它将变成HTTP客户端


如果您必须编写大量测试并在将来支持它们,请考虑是否需要HTTP客户端,这些客户端的语法繁琐,静态配置,应用过滤器和规范的顺序混乱以及容易被破坏的日志记录? 也许是九年前,“ REST安全”是最方便的工具,但在此期间,出现了不具有此类功能的替代产品-Retrofit,Feign,Unirest等。


本文中描述的大多数问题都在大型项目中体现出来。 如果您需要快速编写一些测试而永远忘却它们,而Retrofit不喜欢它,那么REST Assured是最佳选择。


如果您已经在使用REST Assured编写测试,则不必急于重写所有内容。 如果它们稳定且快速,它将花费更多的时间,而不是带来实际的好处。 如果不是这样,REST保证不是您的主要问题。


每天,用DINS为RingCentral API编写的测试数量都在增加,并且仍然使用REST保证。 至少在新测试中,切换到另一个HTTP客户端所花费的时间太大,并且创建的用于配置测试配置的帮助器类和方法解决了大多数问题。 在这种情况下,通过测试维护项目的完整性比使用最漂亮,最时尚的客户端更为重要。 REST尽管有缺点,但仍可以完成其主要工作。

Source: https://habr.com/ru/post/zh-CN464225/


All Articles