Skip to content

brunoziie/kunai

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

kunai

logo

kunai is a pseudo-structural-language designed to test APIs.

kunai provides a high level abstraction that makes it easy to write and run API tests.

Installing

kunai was written in JavaScript and distributed through npm, so make sure you have Node.js installed.

If you don't have Node.js installed yet, no problem. You can go to https://nodejs.org/en/download and download the installer for your platform.

Once you have Node.js installed just run the following command on your terminal.

npm install -g kunai-lang

kunai must be installed globally, so maybe you need to install with administrator privileges. On Linux and MacOS it's made using the following command.

sudo npm install -g kunai-lang

After installed you get access to the kunai command on your terminal.

Getting started

kunai is language, framework or project structure agnostic, that means you can write your test script out of your project. You just need to create a test.kunai file and write your test instructions.

Conventions

Before starting writing is important know some conventions from kunai.

  • kunai is indentation-sensitive, so is mandatory write the test script using 2 spaces indentation. In a near future this will change and the compiler will autodetect the selected indentation pattern.

  • kunai is primary designed to test JSON APIs. You can use to non-JSON APIs too, but you will not be able to access the body as a object. Instead of you will get the body from response as a string.

  • argument-block are blocks defined inside other block.

Writing a simple test

Create a file named test.kunai and put the following code:

CONFIG
  baseurl: 'https://api.github.com/'
  userid: 'brunoziie'
  
GET '/users/:userid:'
  DESCRIBE 'Get user data'

  WITH HEADERS
    User-Agent: 'Kunai API Test'

  RESPONSE MUST
    @assert(response.statusCode == 200, 'Response must have statusCode 200')

  BODY MUST
    @assert(body.gravatar_id, 'User must have a gravatar_id')

Now lets run your test script. On your current directory run the kunai command:

kunai

The instructions you wrote will be executed and will produce the following output:

▶ Get user data
  ✔ Returns statusCode 200
  ✖ User must have a gravatar_id

Voilà! You just ran your first test.

Getting deep

Code structure

kunai has two main types of instruction block, one for define settings and other to define request.

Settings

May you need to share some settings with your requests. You can do it using CONFIG or ENV block. Both do the same thing, it's just a syntax sugar.

Suppose you need to share a user id with all your request. In this scenario you will define something like this:

CONFIG
  user_id: 10283
Predefined variables

kunai has some predefined config variables

CONFIG
  baseurl: 'http://my-api.domain'
  verbose: false
Variable Default Description
baseurl null Prepend your requests url.
verbose false If true, in case of error, will output data about the failed test
Acessing config data

Inside your request in argument-blocks WITH INPUTS, WITH HEADERS and WITH QUERYSTRING you has access to the variable @env. With @env you will be able to access all setting you have defined in CONFIG|ENV block.

CONFIG
  user_id: 10283
  
GET '/users'
  [...]
  
  WITH QUERYSTRING
    id: @env.user_id

In this example you will produce a request with method GET to /users?id=10283

You also use this configs in the path of your request with :variable: notation.

CONFIG
  user_id: 10283
  
GET '/users?id=:user_id:'
# will produce: /users?id=10283
  
GET '/users/:user_id:/posts'
# will produce: /users/10283/

Requests

kunai only implements GET, POST, PUT and DELETE HTTP request methods.

Undestanding request blocks

A full request block can be described as:

[GET|POST|PUT|DELETE] 'my-url-or-segment'
  DESCRIBE 'Description of you request'

  WITH HEADERS 
    # Request headers
    
  WITH QUERYSTRING 
    # Url query string

  WITH INPUTS 
    # Data to send on request

  RESPONSE MUST
    # Rules that response of request have to match
    
  BODY MUST
    # Rules that body of response have to match

  PERSIST
    # Data may you want to persist after request pass on test
     
Defining request params

To make a simple request you must only select a method and give a url, a description and rule to be tested with response or body.

GET 'https://api.github.com/users/brunoziie'
  DESCRIBE 'Description of you request'

  RESPONSE MUST
    @assert(response.statusCode == 200, 'status code must be 200')

  BODY MUST
    @assert(body.gravatar_id, 'User must have a gravatar_id')

  # RESPONSE MUST and BODY MUST are not required individually,
  # but unless one have to be defined

This is the basic, but maybe you need some more complex like to add headers, querystrings or inputs.

Headers

You can add headers using the WITH HEADERS argument-block.

GET 'https://api.github.com/users/brunoziie'
  [...]

  WITH HEADERS
    Content-Type: 'application/json'
    Authorization: 'Token f2dd1408c6a2923f168a0f5d3639ab61'

  [...]
Query Strings

You can add querystring to your URL using the WITH QUERYSTRING argument-block.

GET 'https://api.yummy-recipes.com/recipes'
  [...]

  WITH QUERYSTRING
    page: 1
    search: 'cupcake'

  [...]

This example will produce:

https://api.yummy-recipes.com/recipes?page=1&search=cupcake
Inputs

Request with method POST and PUT may need you pass some data. You can do it using the WITH INPUTS argument-block.

POST 'https://api.yummy-recipes.com/recipes'
  [...]

  WITH INPUTS
    title: 'Awesome Cupcake Recipe'
    recipe: '....'

  [...]
Uploading files

Inside the WITH INPUTS argument-block you has access to @file() method. So you can add file to be uploaded in your request.

POST 'https://api.yummy-recipes.com/recipes'
  [...]

  WITH INPUTS
    title: 'Awesome Cupcake Recipe'
    recipe: '....'
    image: @file('cupcake.jpg')

  [...]

When you uploading file keep in mind that the file path is relative to your current directory.

Defining test params

You can test the response and the body returned from a request. For that you can make use of RESPONSE MUST and BODY MUST argument-block. Both argument-blocks works the same way. The only diference is the scope.

In RESPONSE MUST you will be able to access the response variable with the following data:

Attribute Type Description
response.statusCode number Http status code returned.
response.headers object List of headers sent by server in response
response.request object Object with url, http method and headers sent on request

To test the conditions you'll use the @assert() method.

@assert(condition, description)

In the first argument you will put the condition you want to test and the second argument will be a text explain what you are testing,

POST 'https://api.yummy-recipes.com/recipes'
  [...]

  RESPONSE MUST
    @assert(response.statusCode == 200, 'StatusCode == 200')

  [...]

In BODY MUST you will be able to access the body variable. If the request returns a valid JSON, the body variable will be a object or an array, depending from the returned data, else the body will be a string.

POST 'https://api.yummy-recipes.com/recipes'
  [...]

  BODY MUST
    @assert(body.title == 'Awesome Cupcake Recipe', 'Has the same sent title')

  [...]

You also can access config data in these argument-blocks with the @env variable.

CONFIG
  baseurl: 'https://api.yummy-recipes.com'
  recipe_title: 'Awesome Cupcake Recipe'
  status: 200

POST '/recipes'
  [...]

  RESPONSE MUST
    @assert(response.statusCode == @env.status, 'StatusCode == 200')

  BODY MUST
    @assert(body.title == @env.recipe_title, 'Has the same sent title')

  [...]
Checking types

For other way to validate these tests is using the @it method, follow the these two patters.

Check Description
object, array, string, null, number Verify the type of property
equals, has Verify if the content of property

For this way, is possible to check the propeties inside the object. *All of these features works on RESPONSE MUST or in BODY MUST

Syntax for content property
[...]
  RESPONSE MUST
    @it(response.list).array('The list must be a array')
    @it(response.price).number('The price must be a number')
    @it(response.name).string('The name must be a string')
    @it(response.faults).null('The faults must be a null')
    @it(response).object('The response must be a object')
[...]
Syntax for type property
  [...]
  
  RESPONSE MUST
    @it(response).equals('statusCode', 200, 'The status code must be a 200')

  BODY MUST
    @it(body).equals('statusCode', 200, 'The status code must be a 200')
    
  [...]

Using the function has:

  [...]
  // @it(response).has(path, value, description)
  // @it(response).has(path, description)
  // @it(response).has(path)
  
  RESPONSE MUST
    @it(response).has('statusCode', 200, 'The status code must be a 200')

  BODY MUST
    @it(body).has('statusCode', 200, 'The status code must be a 200')
    
  [...]

When the function has are used firstly the handler check if the property in question exists, after that is verified the value.

You can also use not before of these functions to make a easy and clear check:

  [...]
  
  BODY MUST
    @it(body).not.has(statusCode, 200, 'The status code must be different of 200')
    @it(body.victories).not.null('The victories must be different of null') 

  [...]
Persisting data

Sometimes you need to persist some data from the be used in other requests, like authentication headers or things like that.

For cases like this, you can make use of the PERSIST argument-blocks. Inside this block you will get access to @write()

@write(key, value)
Attribute Type Description
key string Key that will be attributed the value.
value mixed Value to be saved. Can be a string, number, array or object

The @write() method has three more specific methods.

  • @write.qs(key, value): Save querystrings.
  • @write.headers(key, value): Save headers.
  • @write.inputs(key, value): Save inputs.
  • @keepCookies(): Save current response cookies.
CONFIG
  baseurl: 'https://api.yummy-recipes.com'
  recipe_title: 'Awesome Cupcake Recipe'
  status: 200

POST '/login'
  [...]

  PERSIST
    @write.headers('Token', body.user.token)
    @write.qs('category', body.user.category.id) #Assume that's 15
    @write('user_id', body.user.id) #Assume that's 184
  
GET '/users/:user_id:/recipes'
  [...]
  
  # will produce: https://api.yummy-recipes.com/users/184/recipes?category=15

Utils

Spliting test in multiple files.

Maybe writing everything to a file just make the file very long. You can split the test in smaller pieces and include in test.kunai file.

INCLUDE 'users/auth.kunai'
INCLUDE 'users/posts.kunai'
INCLUDE 'users/recipes.kunai'

You can omit the extension of test file.

INCLUDE 'users/auth' # Will be read as users/auth.kunai
INCLUDE 'users/posts.kunai'

About

A pseudo-structural-language designed to test APIs

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published