Quick start
Install the HTTE command line
npm i htte-cli -g
Copy the code
Write the test
Write the configuration config.yaml
modules:
- echoCopy the code
Write test modules/echo. Yaml
- describe: echo get req: url: https://postman-echo.com/get query: foo1: bar1 foo2: bar2 res: body: args: foo1: bar1 foo2: bar2 headers: ! @exist object url: https://postman-echo.com/get?foo1=bar1&foo2=bar2 - describe: echo post req: url: https://postman-echo.com/post method: post body: foo1: bar1 foo2: bar2 res: body: ! @object json: foo1: bar1 foo2: bar2Copy the code
Run the test
htte config.yaml
Copy the code
The execution result
✔ echo get (1s)
✔ echo post (1s)
2 passed (2s)
Copy the code
The original
Why do interfaces need to be tested?
- Improve service quality and reduce bugs
- Locate bugs earlier, saving debugging and processing time
- Easier code changes and refactoring
- Tests are also documentation that helps familiarize you with service functionality and logic
- Service acceptance criteria
There are many projects that do not have interface testing because testing is difficult because:
- Writing tests doubles the work
- There is a learning cost to writing test code
- Data coupling between interfaces makes testing difficult to write
- Constructing request data and verifying response data can be tedious
- Test code is code, and not spending time optimizing iterations is corrupt
Is there a strategy to maximize the benefits of testing while minimizing its costs?
The answer is document-driven.
Documentation describes the tests and implements the documentation with tools.
This is the original intention of HTTE.
Document-driven benefits
It is easier to read
With an interface like this: the service address is http://localhost:3000/add, USES the POST and use json as a data exchange format, the request data format {a: number, b: number}, {c: return data format Number}, which implements the sum of a and b and returns c.
For this interface, test the idea: pass data to this interface {” A “:3,”b”:4} and expect it to return {“c”:7}.
This test is written as a document in the HTTE.
- the describe: two together the req: url: http://localhost:3000/add method: post headers: the content-type: application/json body: a: 3 b: 4 res: body: c: 7Copy the code
A complete list of requests and responses, with a description of what the test does, is written.
It is easier to read
Take a look at the following two tests and guess what the target interface does.
- Describe: login name: fooLogin req: URL: /login method: post body: email: [email protected] password: '123456' res: body: token: ! @exist String-describe: change nickname req: URL: /user method: put Authorization:! $conat [Bearer, ' ', !$query fooLogin.res.body.token] body: nickname: bar res: body: msg: okCopy the code
Although you may not understand it yet! @exist, ! $concat, ! $query, but you should have a rough idea of the functionality of the two interfaces and the format of the request response data.
Because the test logic is documented, HTTE easily gains some of the benefits that other frameworks can only dream of:
Programming language independence
There is no need for the CARE backend to be implemented in that language, and no need to worry about switching from one language to another, let alone from one framework to another.
Low skill requirements, quick hands
Pure documentation, no need to know the backend stack, or even programming. Small white staff, even clerical staff can quickly master and write.
High efficiency, fast development
Easy to write, easy to read, low skill requirements, of course, writing fast. Finally can freely enjoy the advantages of testing, but also the biggest test to avoid trouble.
Naturally suited to test-driven development
Documentation is quick and easy to write, making it easy to adopt a TDD development strategy. Finally, you can enjoy the benefits of TDD without side effects.
As a front-end interface instruction
What if you have a Swagger/Blueprint document but still can’t use the interface? Throw him the test file, full of examples.
As a back-end requirements document/development guidance document
An entry-level employee or junior engineer may not be as familiar with the business, may not be as skilled, may have a long learning or adaptation period, and may not produce quality interfaces. Having such a test document can greatly reduce this time and improve the quality of the interface.
HTTE advantages
HTTE has the following advantages in addition to all the advantages of document-driven testing.
Use the YAML language
Instead of introducing a new DSL, go straight to YAML. There’s no extra learning cost, it’s easier to get started, and you can enjoy existing YAML tools and ecology.
Use plug-ins to generate request validation responses flexibly
Let’s start with why you need plug-ins for document-driven testing.
An interface has duplicate name detection, so when testing we need to generate a random string, how to document the random number? An interface returns an expiration date. We need to verify that the expiration date is 24 hours after the current time. How do we define the expiration date in the documentation?
One of the biggest obstacles to document-driven testing is the lack of flexibility and complexity of documentation to describe probabilities such as random strings and current times. Only functions can provide this flexibility. Plug-ins provide this flexibility by providing functions for documents.
The plug-in is presented as a YAML custom tag.
There’s a code that looks like this
req: body: ! $concat [a, b, c] res: body: ! @regexp \w{3}Copy the code
! $concat and! @regexp is the YAML tag, which is a user-defined data type. In HTTE, it’s just functions. So the above code looks like this to HTTE.
{ req: { body: function(ctx) { return (function(literal) { return literal.join(''); })(['a', 'b', 'c']) } } res: { body: function(ctx, actual) { (function(literal) { let re = new Regexp(literal); if (! Re. The test (actual)) {CTX. Throw (' does not match the regular ')}}) (' \ w {3})}}}Copy the code
In general, the document has the advantages of being easy to read and write, but it cannot bear complex logic and is not flexible enough. Functions/code provide this flexibility, but with too much complexity. HTTE’s use of the YAML format, which encapsulates functions in YAML tags, reconciles this contradiction, maximizing each other’s strengths and almost avoiding each other’s weaknesses. This is HTTE’s biggest innovation.
There are two main operations on data in interface testing, constructing a request and validating a response. So there are two types of plug-ins in HTTE.
- Constructor (resolver), used to construct data, tag prefix
! $
- Differ, used to compare parity data, label prefix
! @
The plug-in set:
- Builtin – Contains some basic commonly used plug-ins
Componentized, easy to expand
The HTTE architecture diagram is as follows:
Each component is an independent module that performs a specific task in opposition. So it’s easy to replace, and it’s easy to extend.
Here is an example of how a test unit executes in HTTE to familiarize you with the functionality of each component.
Here’s another test
- describe: req: body: v: ! $randnum [3, 6] res: body: v: ! @compare op: gt value: 2Copy the code
After being loaded by Runner, all YAML tags are expanded into functions according to the plug-in definition, with pseudo-codes as follows. Runner sends the runUnit event at the same time.
{
req: { // Literal Req
body: {
v: function(ctx) {
return (function(literal) {
let [min, max] = literal
return Math.random() * (max - min) + min;
})([3, 6])
}
}
},
res: { // Expect Res
body: {
v: function(ctx, actual) {
(function(literal) {
let { op, value } = literal
if (op === 'gt') fn = (v1, v2) => v1 > v2;
if (fn(actual, literal)) return;
ctx.throw('test fail');
})({op: 'gt', value: 2})
}
}
}
}Copy the code
The Runner passes the Literal Req to the Resolver, whose job is to recursively traverse the functions in the Req and execute them to get a pure value of data. And pass it to the Client.
req: { // Resolved Req body: { v: 5; }}Copy the code
The Client receives this data, constructs the request, encodes the data in an appropriate format (in the case of JSON, Encoded Req becomes {“v”:5}), and sends it to the back-end interface service. After receiving the response from the back-end service, the Client decodes the data. If the interface is a echo service and returns {“v”:5}(Raw Res) with JSON, the Client decodes the data as:
res: { // Decoded Res body: { v: 5; }}Copy the code
Differ will now get Expected Res from Runner and Decoded Res from Client. Its job is to compare the two.
Differ will traverse every Expected Res value and compare one by one with the Decoded Res. Any discrepancy will throw an error, marking a test failure. If both are values, determine whether they are congruent. If a function is encountered, the comparison function is performed. The pseudocode is as follows:
(function(ctx, actual) {
(function(literal) {
let { op, value } = literal
if (op === 'gt') fn = (v1, v2) => v1 > v2;
if (fn(actual, literal)) return;
ctx.throw('test fail');
})({op: 'gt', value: 2})
}
})(ctx, 5)Copy the code
If the alignment function does not throw an error, the test passes. The Runner receives a test pass and sends the doneUnit event and executes the next test in the queue.
Reporter listens for events sent by Runner, generates corresponding reports, or prints to terminals, or generates HTML report files.
The interface protocol is extensible and currently supports HTTP/GRPC
The interface protocol is provided by the client extension.
- Htte – Suitable for TESTING HTTP interfaces
- GRPC – Suitable for GRPC interface testing
Report generator is extensible and currently supports CLI/HTML
- Cli – Output to the command line
- HTML – Outputs test reports as HTML files
Elegant solution to interface data coupling
There is coupling of data between interfaces. For example, you can log in and get the TOKEN before you have the permission to place an order or post it in moments.
So one interface test often needs to access another test’s data.
HTTE handles this problem through sessions + plug-ins.
I’m going to give you an example.
There is a login interface, and it looks like this.
-describe: Tom login name: tomLogin # <-- Register a name for the test, Why? req: body: email: [email protected] password: tom... res: body: token: ! @exist stringCopy the code
There is an interface to change the user name. It is the permission interface and must have an Authorization request header and a token returned from login to use it.
- describe: tom update username to tem req: headers: Authorization: ! $conat [Bearer, ' ', token?] How to become a TOKEN? body: username: temCopy the code
Your answer
Authorization: ! $conat [Bearer, ' ', !$query tomLogin.res.body.token]Copy the code
Can also through tomLogin. The req. Body. Email mailbox value, through tomLogin. The req. Body. The password for the password. Is it elegant?
How does this work?
After the Runner starts in HTTE, the session is initialized. After each unit test is executed, the execution results are recorded in the session, including request data, response data, elapsed time, test results, and so on. This data is read-only and is exposed to the plug-in function as CTX, so the plug-in can access the data from the tests it previously performed.
In the same test, data from the REQ can also be referenced in the RES.
- describe: res ref data in req req: body: ! $randstr res: body: ! @query req.bodyCopy the code
Use macros to reduce repetitive writing
There are often multiple unit tests around an interface, and interfaces have some consistent properties. Take HTTP interfaces for example, req.url, req.method, req.type.
- decribe: add api condition 1
req:
url: /add
method: put
type: json
body: v1...
- decribe: add api condition 2
req:
url: /add
method: put
type: json
body: v2...Copy the code
Macros were introduced to solve this problem of repeated input. Use is also simple, define + reference.
Define macros in the project configuration.
Req: url: /add method: put type: jsonCopy the code
You can use this interface anywhere, that’s all you need
Describe: add API with macro includes: add #Copy the code
Develop while debugging
This feature is implemented by combining command-line options. The two command line options are: — Bail stops execution if any tests fail; –continue Tests continue where they were last interrupted.
Combining these two options allows us to reset the interface that executes the problem numerous times until debugging passes.
configuration
Optional configuration items:
session
HTTE will generate a project-unique temporary file storage session in the temporary folder of the operating systemmodules
: Provides a list of test module files. The list order corresponds to the execution order. HTTE will be in the directory where the configuration file is locatedmodules
Directory finds and corresponds to module files for the current directory.clients
: Configures client extensionplugins
: Configure plug-insreporters
: Configures the report builder extensiondefines
To define the macro
Minimum configuration:
modules:
- auth
- user/orderCopy the code
Clients, plugins, and Reporter all use default values.
Fully configured:
session: mysession.json
modules:
- auth
- user/order
clients:
- name: http
pkg: htte-client-http
options:
baseUrl: http://example.com/api
timeout: 1000
reporters:
- name: cli
pkg: htte-reporter-cli
options:
slow: 1000
plugins:
- name: ''
pkg: htte-plugin-builtin
defines:
login:
req:
method: post
url: /users/loginCopy the code
Configuration patches are used to cope with configuration changes due to environment differences. Such as write test environment, interface address to http://localhost:3000/api; In a formal environment, change the value to https://example.com/api. It is best to configure the patch implementation.
Define patch file, patch file naming rules.. . If the project configuration file is htte.yaml and the patch name is prod, the patch file is htte.prod.yaml.
- op: replace
path: /clients/0/options/baseUrl
value: https://example.com/apiCopy the code
Use the –patch option on the command line to select the patch file. For example, to reference htte.prod.yaml, type –patch prod
Patch file specification jsonPatch.com.
Test units/groups
The test unit is the basic unit of the HTTE.
describe
: Describe the purpose of the testname
: Defines the test name for convenience! $query
和! @query
Reference, can be omittedclient
: defines the use of client extensions, which can be omitted if the project has only one clientincludes
: reference macro, can reference more than onemetadata
: meta label, dedicated data for HTTE engineskip
: Whether to skip this testdebug
: True indicates details of the request and response data printed at the time of the reportstop
: True: Terminates subsequent operations after the test
req
: Request to view the corresponding client extension documentres
: In response, refer to the corresponding client extension document
Sometimes a functional test requires multiple test units to complete. To express this composition/hierarchy, HTTE introduces the concept of groups.
describe
: Describes the purpose of the testdefines
: defines intra-group macros with the same syntax as global macros in the configurationunits
: Group element
- descirbe: group defines: token: req: headers: Authorization: ! $conat [Bearer, ' ', !$query u1.req.body.token] units: - describe: sub group units: - descirbe: unit name: u1 metadata: debug: true includes: login req: body: username: foo passwold: p123456 res: body: token: ! $exist - descibe: unit includes: [updateUser, token] req: body: username: barCopy the code
license
MIT