You can support this site from as little as $1 a month on Patreon.

The support pays for site hosting and the expansion of this content.

In addition, Patreon Supporters gain access to exclusive online video training courses and ebooks.

Join Now

Bruno

Bruno is an Open Source API client designed for local use, with Git as the main project sharing mechanism.

About Bruno

Bruno is an Open Source API client with paid features. The free features have been good enough for everything that I have had to do with Bruno.

Bruno can be found at:

Bruno is a fast developing project so to get a full understanding of the tool I read the documentation, the blog and the Github feature request forums.

Demo Video of Bruno

Watch on YouTube: "Bruno API Client Overview"

Patreon ad free video

Benefits of Bruno

I find Bruno to be easy to use. Bruno focuses on making it easy to interact with an API either interactively using the UI or automating using the Test tabs.

I have not used the Test capabilities because I automate using HTTP libraries. But I have used the Scripting capabilities to generate random data and log the output of my testing.

Quick Review Criteria

Feature Y/N Comment
Optional
Read Open API files Y Also populates Environment host variables
Supports global environment variables e.g. for host Y With syntax highlighting and linting
Different payload body types Y
Easy to add headers Y
- custom headers Y
- auth header support Y
- override body type headers Y It is possible to edit as JSON and set content-type header to XML
Variables in headers Y
Custom Verb support N GET, POST, PUT, DELETE, PATCH, OPTIONS, HEAD
Repeating or cloning earlier requests Y But not from the History, only the Request UI
View history of requests/responses Y But no export capability
Essential
Send HTTP or HTTPS requests Y
Proxy support Y
Can create invalid requests Y
Bonus
Data driven requests Y Paid plan only
Output log of test sessions N
Scriptable for customisation Y At Collection and Request level

Bruno met all my basic needs. The Data Driven feature is only available on a paid plan.

For people who like to automate in the REST Client, the test suite capability can be used on the free plan.

Notes on Usage

Bruno saves all the collection files in a folder. This makes it easy to review and manage in version control.

I haven't used the direct Git integration features in Bruno, preferring to manage the Collection versioning using Git directly.

Bruno makes the version control easy by keeping all data in simple files, and multiple files are used rather than a single database file or large JSON or XML file.

When I imported an Open API file with environment data the environment variables in Bruno were set correctly and I was able to use the remote server or local server without configuring the collection in the UI.

The Payload body editor uses the syntax of the payload type selected e.g. JSON payloads are colour coded and syntax checked.

The editor did not prevent me from sending through invalid payloads. i.e. the payload was shown as invalid in the UI but still sent through to the server which is exactly what I want.

Headers are easy to add. Code completion on Headers is available for the value, rather than the key.

Multiple authentication types are supported by the UI making it easy to add Bearer tokens or API keys. These can also be added directly through the Header amendment and can use values in variables.

Variables can be created at multiple levels:

  • Collection
  • Request
  • Environment
  • Local OS Environment

I tend to stick to Environment variables. But have experimented with Collection variables for random data.

Bruno has recently added Dynamic Data so Faker can be used to add random data into a payload.

e.g. {{$randomCurrencyCode}}

This is documented in the Dynamic Variables section.

Bruno changes frequently.

Conclusion

Bruno is my current default REST API tool for Exploratory and Interactive Test Sessions.

I find it:

  • easy to use.
  • plays well with other tools e.g. Proxies
  • easy to script and generate random data
  • imports Open API files well
  • has a small set of features to the UI is uncluttered

The paid plan as the Data Driven capabilities. But if I want Data Driven I either use a Proxy or code the automated execution.

Appendix - Experiments in Scripting Bruno

I use the Scripting capabilities of tools on an ad-hoc basis to improve my workflow, rather than automating the coverage.

I found the Scripting in Bruno to be very suitable for my purposes.

Bruno comes with useful libraries built in, and it is possible to treat your Bruno collection as a Node project by adding a package.json and adding whatever library you want.

Random Data

I first experimented using Random Data in Bruno. The built in Faker {{$randomCurrencyCode}} capability is useful but I wanted more custom random data.

e.g. the Simple API requires a unique ISBN number for each item

I used Bruno to make a random ISBN available to each request, if I wanted to use it.

In the Script section of my collection I added:

bru.setVar("isbn13",'xxx-x-xx-xxxxxx-x'.
    replaceAll('x', (match, p1, offset) => {return Math.floor(Math.random() * 10)}));

This means on every Request, there is a variable called isbn13 which has been set to a random valid ISBN 13 format, which I could use in the request by adding {{isbn13}} anywhere in the request. This gave me the ability to test quickly when I didn't care what ISBN to use, and I could easily use a String when I wanted more control.

I noticed that one of the supported libraries when the collection is opened in Developer mode is lodash. so I used lodash to randomly select a 'type' from an Array. I could have used normal JavaScript for this but wanted to experiment with the Scripting capabilities.

I found I had to require lodash as follows:

var _ = require('lodash');
bru.setVar("type",_.sample(['cd', 'dvd', 'blu-ray','book']));

The code above created a {{type}} variable with a random type if I wanted to use it in a payload.

e.g. in a POST payload

{
  "type": "{{type}}",
  "isbn13": "{{isbn13}}",
  "price": "13",
  "numberinstock": "12"
}

The Scripting approach was well documented and easy to use.

Logging Test Sessions

I started writing a script to run at a Collection level at a post response event level (after every response).

I wanted to write the request and response object to a file to keep track of my testing.

I found that I had to use a feature that is documented but not obviously documented.

Amending the bruno.json file in the collection to allow scripts to access the file system.

 "scripts": {
   "filesystemAccess": {
     "allow": true
   }
 }

This is shown in the documentation in some code sections but I found it by searching Github for other questions related to file system access through Scripting.

  • https://github.com/usebruno/bruno/issues/306

This is also when I found that there is a section in the Discussions where people have uploaded their own scripts.

This is where I found a script that closely matched what I wanted:

  • https://github.com/usebruno/bruno/discussions/385#discussioncomment-7246390

The above script:

  • requires creating a logs folder in the collection
  • writes a JSON representation of the request and response to a time stamp named file

I amended the script to:

  • create the logs folder
  • write all the responses to a markdown formatted file
  • with one file for each hour
  • if a TestNote variable is in the Request section then it will be written as a paragraph in the log output

This more closely matches my Test Approach, where I Test for small chunks of time, and create notes in a text editor about the work I'm doing.

The script I used is below, and it was pasted into the Collection post response Script section:

// based on https://github.com/usebruno/bruno/discussions/385#discussioncomment-7246390

const fs = require('fs');
const path = require('path');
const moment = require('moment');

const safeStringify = function (obj) {
  try {
    return JSON.stringify(obj, null, 2);
  } catch (err) {
    return obj
  }
};

const request = {
  url: req.getUrl(),
  headers: req.getHeaders(),
  method: req.getMethod(),
  body: req.getBody()
};

const response = {
  status: res.getStatus(),
  headers: res.getHeaders(),
  body: res.getBody()
};

const now = moment();
const fileNameFormattedDate = now.format('YYYYMMDD-HH');

const dir = path.join(bru.cwd(), 'logs')
if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir);
}

const filename = path.join(dir, `${fileNameFormattedDate}.md`);
const content = safeStringify({request, response}, null, 2);

let title = "\n\n## " + new Date().toLocaleString() +
  " - " + res.getStatus() + " " + 
  req.getMethod() + " " + req.getUrl() +
  "\n\n";

fs.appendFileSync(filename, title);

let comment = bru.getRequestVar("TestNote")

if(comment!=null){
  fs.appendFileSync(filename, "\n\n" + comment + "\n\n");
}

fs.appendFileSync(filename, content);