Htte is a declarative HTTP testing framework.

Translation: English | Chinese

After writing the API, we need to test its functionality. Testing Postman and Curl manually is another option, but the drawbacks are obvious. Serious programmers often write tests to do this. Typically, request data is generated, the request is sent over HTTP, and the response matches expectations.

The following is an example of a node.js interface for registering and modifying user information:

const { assert } = request('chai')

let registJohnToken

it('regist', function() {
  return request(app)
    .post('/api/users')
    .set('Accept', 'application/json')
    .send({
      user: {
        email: '[email protected]',
        password: 'johnnyjacob',
        username: 'johnjacob'
      }
    })
    .expect(200)
    .then(response => {
      let user = response.body.user
      assert.isDefined(user)
      assert.equal(user.email, '[email protected]')
      assert.equal(user.username, 'johnjacob')
      assert.isDefined(user.token)
      registJohnToken = user.token
    })
});

it('update user data', function () {
  return request(app)
    .put('/api/user')
    .set('Accept', 'application/json')
    .set('Authorization', 'Token ' + registJohnToken)
    .send({
      user: {
        username: 'john'
      }
    })
    .expect(200)
    .then(response => {
      let user = response.body.user
      assert.isDefined(user)
      assert.equal(user.username, 'john')
    })
})Copy the code

How do you implement these two tests using HTTE?

First we need to write a configuration file.htte.yaml to describe the interface

url: http://localhost:3000/api
apis:
  register: 
    method: post
    uri: /users
  updateUser:
    method: put
    uri: /userCopy the code

Then we write the test user.yaml

units: - describe: register user api: register name: registerJohn req: body: user: email: [email protected] password: johnnyjacob username: johnjacob res: body: user: email: [email protected] username: johnjacob token: ! @exist - describe: update user data api: updateUser req: headers: Authorization: ! $concat ['Token', ' ', !$query ?registerJohn.res.body.user.token] body: user: username: john res: body: user: ! @object username: johnCopy the code

Rewrite done.

We can run tests using the HTTE command line tool

NPM I -g htte install RunUnits: user: regist user: ✓ Update user data: ✓Copy the code

The interface above was captured from the RealWorld project. This project uses HTTE for interface testing and can be used as an example to learn how to use HTTE.

features

  • Write tests using the declarative approach (YAML)
  • Not coupled to backend development languages
  • No skills required, no programming skills required to write tests
  • Straightforward description of the data required by the request and the data returned by the response, simple, easy to understand, has been written
  • You can use plug-ins to customize the generation of request data and assert response data, providing flexibility and simplicity
  • Provides jSONPath variable mechanism to handle data coupling between test interfaces, which is simple, convenient and safe

content

  • configuration
  • The test code
    • Rely on
    • The test set
    • The test unit
  • The plug-in
    • Why plug-ins are needed
    • How does the plug-in work
    • Types of plug-ins
    • Built-in plug-ins
  • variable
    • Why you need variables
    • How are variables defined
    • Variable name rule
    • The global variable
  • The command line
    • run
    • inspect
    • view

configuration

The HTTE needs to know configuration information such as where to load the test file from, which interfaces are available and how to send requests, and which plug-ins need to be loaded in order to do the testing. The configuration information is stored in a configuration file in YAML format. The configuration file is specified using the command line parameter -c or –config.

All configurable items and their explanations are listed below.

RootDir: '.' # specifies the root directory where the test files reside. Htte recursively reads all yamL files in this directory and parses them as test modules. SessionFile:./.session # The file used to persist the session. Htte uses this file to record the request and response data of the test and the url of the test interrupt location: http://localhost:3000 # denotes the base addresses of all interface paths. When interface paths are relative, this address can be connected to get complete paths: [] # a list of NPM modules that register transcoders with HTTE and set the 'Content-Type' request headers, transcoded request data, and decoded response data. Type: json # 'type' specifies the default transcoder type. Timeout: 1000 # Set the request timeout period, in milliseconds, after which the request will be automatically interrupted with an error. Plugins: [] # A list of NPM modules that add plugins to HTTE.Copy the code

Apis are required fields that represent a set of interface information.

Interface information: request name, request path (required), request method (default get), request data transcoding type, timeout.

You can use hash tables

apis:
  register:
    method: post
    uri: /users
    type: 'json'
    timeout: 300
  getUser: /userCopy the code

You can also use an array

apis:
  - name: register
    uri: /users
    method: post
    type: 'json'
    timeout: 300
  - name: getUser
    uri: /userCopy the code

The test code

Putting all the test code in one file is obviously not a good option. Htte allows test code to be spread across multiple files.

Each file will be treated as a module.

A module consists of two parts: module dependencies and test sets Units.

Rely on

Dependencies are used to ensure the order of execution.

The interface is executed in an orderly manner, and users must register to publish articles. Htte loads modules by directory, the order of modules being sorted first by file path and then by dependencies. For example, module Auth contains tests for registering and logging related interfaces, module Article contains tests for user article CRUD related interfaces, only registered users can publish articles, so module Auth should be ranked before module article. This order is guaranteed by setting the Article module to depend on the Auth module.

The value of the field Dependencies consists of a set of dependencies that have attributes: module path, module reference name. The module path is required, and the module reference name is optional. If no module reference name is specified, the HTTE uses the module name automatically generated from the module path.

The value of the field Dependencies can be an array,

dependencies:
  - ./auth.yaml
  - name: article
    module: ./article-authenticated.yamlCopy the code

You can also use hash tables,

dependencies:
  auth: ./auth.yaml
  article: ./article-authenticated.yamlCopy the code

There are a few things to note about dependencies:

  • If there is a sequential dependency, the HTTE will recognize it and report an error directly.
  • Dependency affects variable queries within the module. Only when you add a dependency can you use the data in the dependent module and use the corresponding variables.

The test set

Test sets and contain unit tests and subtest sets

units: - describe: feed api: getFeed req: headers: Authorization: ! $concat ['Token', ' ', !$query $auth.loginJohn.res.body.user.token] res: body: articles: [] articlesCount: 0 - describe: article without auth units: - describe: all articles api: listArticlesCopy the code

The top-level test set contains the unit test feed and the Article Without Auth sub-test set, which contains a unit test All Articles. A test set can contain any number of unit tests and subtest sets at any level.

The test unit

The test unit describes how requests are made and how responses are compared and validated.

An example unit test:

Describe: this field must be used to describe an API that is covered by 'apis' in the configuration. This field must be named: Params: {slug: 'htte', id: 3} # Request URL path parameter. headers: { Authorization: 'Bearer ... Query: {page: 3, size: 20} # Request path queryString, in hash form. body: { content: 'go! go! go! '} # request data res: # Response data status: 200 # Response status code, if null, asserts that the response code is in the range 200-299. Headers: {content-length: '26'} @object { json: { content: 'go! go! go! '}} # response dataCopy the code

If its interface looks like this

method: post
name: endpoint
uri: https://httpbin.org/anything/{slug}/{id}
Copy the code

The above test generates the following request:

curl \ -X POST \ -H 'Content-Type: application/json' \ -H 'Authorization: Bearer ... ' \ -d '{"content": "go! go! go!" }' \ https://httpbin.org/anything/htte/idCopy the code

Htte will perform the following verification:

  • Response status code 200
  • The response header contains the content-Length field with a value of 26
  • The response body is an object that has a field JSON whose value is also an object{content: 'go! go! go! '}

If all the checks pass, the test passes. If one check fails, the test fails.

The following points need special attention:

  • onlydescribeapiIs a must
  • req.paramsMust correspond to the path parameter in the API object. For example, API objects have paths/articles/{slug}/comments/{id}.req.paramsMust have attributes slug and ID
  • Unit testingnameOnly if this field is provided can other tests in this module and other modules reference the data in this test via variables

The plug-in

Why plug-ins are needed

The so-called interface test is to give the request data and then compare the response data.

Sometimes we cannot directly give the request data, such as the current time. Sometimes we are not completely sure of the response data, for example the return value may be followed by a random field. Tests are flexible, but description documentation is rigid. We can’t write down the current time. Maybe we could agree on a symbol to represent the current time, but who knows what similar problems we might encounter in the future? Obviously it’s not realistic to agree on a symbol without first encountering the problem.

We need functions, and only functions have the flexibility to meet these challenges.

Plug-ins provide functions, and we need plug-ins.

How does the plug-in work

Htte uses YAML, and in order to understand how plug-ins work, we must first learn some of the YAML specifications.

In YAML, there are four data types.

  • Scalar (scalar)
  • Array (sequence)
  • Hash table (mapping)
  • Label (tag)
scalar: 3 sequence1: - 1 - 2 sequence2: [1, 2] mapping1: country: china captial: beijing mapping2: { country: china, captial: beijing } tag: ! $now 86400000Copy the code

Labels must be registered before they can be used. Registering tags requires passing data to the parsing engine: tag names, parameter data types, and constructors. When the parsing engine encounters a tag, it parses the data behind the tag and calls the tag’s constructor as an argument, with the constructor execution result as the final value. Functions appear in yamL files as labels.

Registering plug-ins is like registering tags in the YAML parsing engine. Plug-ins provide functions in the form of YAML tags.

Types of plug-ins

Htte provides two types of plug-ins

  • The resolver is used to generate request data, and its YAML tag is prefixed! $, like! $now returns the current time string

  • Differ is used to compare validating response data, and its YAML tag is prefixed! @, such as! @regexp Verifies whether the data matches the re

Built-in plug-ins

Htte provides built-in plug-ins that do not require additional installation and can be used directly.

The functions and basics of these plug-ins are useful in general testing. They are also examples that you can use to write your own plug-ins. If you find an important feature that should be included in the built-in plug-in, feel free to submit an issue or pull Request.

! $query

! $query: Queries the variable value, with the parameter type scalar

! $query $auth. Login. The req. Body. The user. The # token return auth module called login test response data of the user. The token valueCopy the code

! $concat

! $concat: Concates a sequence of strings

! $concat: [a, b, c] # "abc" ! $concat: [Bearer, ' ',! $query $auth. Login. The req. Body. The user. The token] # "Bearer < > token value", and other parameters can be nested resolverCopy the code

! $now

! $now: Specifies the current time. The parameter type is scalar

Takes an argument, offset, which represents the current time offset millisecond tree

! $now # 2018-04-25T02: 29:03.572z $now # 2018-04-25T02: 29:03.572z $now 86400000 # 2018-04-26T02: 29:03.572z, tomorrow! $now-86400000 # 2018-04-24T02: 29:03.572z, yesterdayCopy the code

! $randstr

! Randstr: a random string with the parameter type scalar

Takes a single argument, length, which represents the random string length

! $randstr # 5xa4Wi ! $randstr # Qdf4dY ! $randstr 8 # 9sw2DhxHCopy the code

! @query

! @query: Whether it is equal to the variable value, with the parameter type scalar

res: body: email: ! @query ? $res.body.emailCopy the code

True if the response data has a field email value equal to the email value of the request data

! @exist

! @exist: Whether a field exists with the parameter type scalar

res: body: username: ! @exist token: ! @existCopy the code

If the test returns an object with and only the attributes username, token, true. You don’t care about the value, you just care about the presence of the attribute.

! @regexp

! @regexp: Whether the re is matched or not, with the parameter type scalar

res: body: slug: ! @regexp /^how-to-train-your-dragon-\\w{6}$/Copy the code

True if the response data has the field slug, which is a string and matches the re

! @array

! @array: indicates partial array verification. The parameter type is mapping

res: body: ! @array 0: dragonCopy the code

If the response data field value is array and the first element value is dragon, true here we are only interested in the first element, so only partial verification is done.

If we are still interested in the length of the array, we can assert the length

res: body: ! @array 0: dragon length: 4Copy the code

Htte performs parity check on arrays by default. This means that all elements must be listed in the correct order and value.

! @object

! @object: Partially verifies an object. The parameter type is mapping

res: body: ! @object email: john@jacobCopy the code

True if the response data uses the field email with the value john@jacob. If omitted! The @object tag indicates that the data has only the field email.

Htte performs parity check on objects by default. You can’t have too many fields or too few fields.

variable

Variables are a mechanism for accessing request and response data for completed tests.

Why you need variables

Web services are generally stateful, resulting in data relationships between interfaces. Users need to register if they want to publish. Because the interface to publish articles needs to register the token data in the interface. You need a mechanism to retrieve the data from the previous tests. This mechanism is called a variable.

How are variables defined

When the HTTE performs unit tests, it generates the relevant request and response data, which is recorded in the session and persisted to the disk sessionFile.

For example, the unit test registJohnToken (code in the header of the article) produces the following record:

{ auth: { registerJohn: { req: { body: { user: { email: '[email protected]', password: 'johnnyjacob' username: 'johnjacob' } } }, res: { status: 200, headers: { Content-Type: 'application/json', ... }, body: { user: { username: 'johnjacob' email: '[email protected]' token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJqb2huamFjb2IiLCJleHAiOjE1Mjk4MjMyNDEsImlhdCI6MTUyNDYzOT I0MX0.hdzrrn1wk9M7ba1WBugoWjtp-suG1d4UVW0hhf-aD8E' } } } } } }Copy the code
  • authIs the module name, which is automatically generated based on the module path
  • registJohnTokenFrom unit testsnamefield
  • reqRequest data corresponding to unit tests
  • resIs the response data returned by the Web service

You can use the HTte Inspect command line tool to view unit test information

This is a huge object, but we often only need a specific value, such as token, such as username. Creating a variable for each required data is tedious. And you may write tests without knowing that those will be variables. To obtain the value of a particular data, HTTE uses JSONPath to locate the data. The access path of the data is the name of the variable, which does not need to be declared or bound. Any part of the data can be accessed as a variable.

We need to get the value of the token field, Its jsonpath is auth. RegisterJohn. Res. Body. The user. The token, variable called $auth. RegisterJohn. Res. Body. The user. The token, obtained by label: yaml! $query $auth. RegisterJohn. Res. Body. The user. The token.

Variable name rule

The variable name is the JsonPath of the data. To enable cross-module access to data, the variable name contains the module name and the test name, prefixed with $.

If you reference data from tests in this module, omit the module name,? RegisterJohn. Res. Body. The user. The token, prefix? .

If the res internally references reQ data within the same test, you can omit the test name. $the req. Body. The user email, prefix? $.

The global variable

The data for the global variable comes from the variables field in the configuration, again with jsonPath as the variable name. The prefix?? .

The command line

Global options

  • Config: indicates the path of the project configuration file, default./.htte.yaml

run

Run the test, which is the default command. Htte is equivalent to htte run.

Options:

  • Debug: Prints request and response data
  • Amend: Run the tests from where they were last interrupted
  • Bail: If the bail fails the test, the subsequent test will be interrupted
  • Unit: Runs tests from the specified unit tests
  • Shot: Executes a single test and does not run subsequent tests

inspect

View a specific unit test and display all information about that test

$ htte inspect auth-registerJohn name: registerJohn module: auth api: name: register method: post url: 'http://localhost:3000/api/users' timeout: 1000 type: json keys: [] req: body: user: email: [email protected] password: johnnyjacob username: johnjacob res: status: 200 headers: x-powered-by: Express access-control-allow-origin: '*' vary: X-HTTP-Method-Override content-type: application/json; charset=utf-8 content-length: '237' etag: W/"ed-/KWrmVNj/2bN/mK81GyveA" date: 'Fri, 27 Apr 2018 08:41:44 GMT' connection: close body: user: username: johnjacob email: [email protected] token: >- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJqb2huamFjb2IiLCJleHAiOjE1MzAwMDI1MDQsImlhdCI6MTUyNDgxODU WNH0. WF - 5 h8bhnzqmvr8fxzpp6ifcysr0 - vQ7T6p1iTQ7tJ0 time: 312.791432Copy the code

view

Looking at the tests, you can see all the tests and their structure.

Options:

  • Module: Displays only tests under a specific module
  • API: Tests that show only characteristic apis
ViewUnits: 
  user:
    regist user:
      registerJohn
    update user data:
      updateUser-1
Copy the code

license

MIT