API Testing
Test Evolve can be configured to perform API testing. You will find an example within a new Test Evolve Spark project.
Instructions
Install a node package such as Axios for making API requests.
- TypeScript
- JavaScript
- Ruby
npm install axios
npm install axios
gem install rest-client
Write your API test with a Cucumber feature and scenario.
Feature: Example API Test Collection using the Chronicling America Open API
@example_api_test
Scenario: Users request returns articles that reference a particular location
Given I search for news articles that reference 'Michigan'
Then I receive results that are relevant to my search term
- Write your step definitions.
- TypeScript
- JavaScript
- Ruby
features/step_definitions/apiTestSteps.ts
import {Given, Then} from "@cucumber/cucumber";
import * as apiHelper from '../support/api/apiHelper';
Given(/^I search for news articles that reference '(.*)'$/, async function (data) {
this.searchTerm = data
this.results = await apiHelper.findArticles(this.searchTerm);
});
Then(/^I receive results that are relevant to my search term$/, async function () {
apiHelper.exampleDataOutputsFromResponse(this.results);
apiHelper.assertResultsMatch(this.results, this.searchTerm);
});
features/step_definitions/apiTestSteps.js
import {Given, Then} from "@cucumber/cucumber";
import * as apiHelper from '../support/api/apiHelper';
Given(/^I search for news articles that reference '(.*)'$/, async function (data) {
this.searchTerm = data
this.results = await apiHelper.findArticles(this.searchTerm);
});
Then(/^I receive results that are relevant to my search term$/, async function () {
apiHelper.exampleDataOutputsFromResponse(this.results);
apiHelper.assertResultsMatch(this.results, this.searchTerm);
});
features/step_definitions/api_test_steps.rb
Given(/^I search for news articles that reference '(.*)'$/) do |search_term|
@search_term = search_term
@results = APIHelper.find_articles(search_term: @search_term)
end
Then(/^I receive results that are relevant to my search term$/) do
if @results
APIHelper.example_data_outputs_from_response(@results)
APIHelper.assert_results_match(@results, @search_term)
end
end
- Create methods/functions for making API requests and handling responses. Call these functions from your step definitions.
- TypeScript
- JavaScript
- Ruby
features/support/apiHelper.ts
import axios from 'axios';
import {testEvolve} from "@testevolve/testevolve-spark";
interface Article {
title: string;
start_year?: string;
holding_type?: string[];
url?: string;
};
interface ApiResponse {
items?: Article[];
};
export const findArticles = async (searchTerm: string = "new orleans"): Promise<ApiResponse> => {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
try {
let config = {
method: 'get',
maxBodyLength: Infinity,
url: `https://chroniclingamerica.loc.gov/search/titles/results/?terms=${searchTerm}&format=json`,
headers: headers
};
const response = await axios.request<ApiResponse>(config);
testEvolve.log.info('API RESPONSE CODE: ', response.status);
const data = response.data;
testEvolve.log.info('API RESPONSE BODY: ', data);
return data;
} catch (error) {
throw new Error(error);
}
}
export const assertResultsMatch = (responseData: ApiResponse, searchTerm: string): void => {
const normalise = (str: string): string =>
str.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
const normalisedSearchTerm = normalise(searchTerm);
if (!responseData.items || !Array.isArray(responseData.items)) {
throw new Error('Invalid response data: "items" array not found.');
}
const failingItems = responseData.items.filter((item) => {
const normalisedTitle = normalise(item.title);
return !normalisedTitle.includes(normalisedSearchTerm);
});
if (failingItems.length > 0) {
const failingTitles = failingItems
.map((item) => `- ${item.title}`)
.join('\n');
throw new Error(
`The following titles do not contain the search term "${searchTerm}":\n${failingTitles}`
);
}
};
export const exampleDataOutputsFromResponse = (responseData: ApiResponse): void => {
if (!responseData.items || responseData.items.length === 0) {
testEvolve.log.warn("No articles found in response.");
return;
}
if (responseData.items[0]) {
testEvolve.log.info('Extract the first object: ', responseData.items[0]);
testEvolve.log.info('Extract the "start_year" of the first object: ', responseData.items[0].start_year);
testEvolve.log.info('Extract the first "holding_type" of the first object: ', responseData.items[0].holding_type?.[0]);
}
if (responseData.items[1]) {
testEvolve.log.info('Extract the second object: ', responseData.items[1]);
}
if (responseData.items[2]) {
testEvolve.log.info('Extract the "url" of the third object: ', responseData.items[2].url);
testEvolve.log.info('Extract the second "holding_type" of the third object: ', responseData.items[2].holding_type?.[1]);
}
};
features/support/apiHelper.js
import axios from 'axios';
import {testEvolve} from "@testevolve/testevolve-spark";
export const findArticles = async (searchTerm = "new orleans") => {
let headers = {
'Accept': 'application/json',
'Content-Type': 'application/json'
};
try {
let config = {
method: 'get',
maxBodyLength: Infinity,
url: `https://chroniclingamerica.loc.gov/search/titles/results/?terms=${searchTerm}&format=json`,
headers: headers
};
const response = await axios.request(config);
testEvolve.log.info('API RESPONSE CODE: ', response.status);
const data = response.data;
testEvolve.log.info('API RESPONSE BODY: ', data)
return data;
} catch (error) {
throw new Error(error);
}
}
export const assertResultsMatch = (responseData, searchTerm) => {
const normalise = (str) =>
str.toLowerCase().replace(/[^a-z0-9\s]/g, '').trim();
const normalisedSearchTerm = normalise(searchTerm);
if (!responseData.items || !Array.isArray(responseData.items)) {
throw new Error('Invalid response data: "items" array not found.');
}
const failingItems = responseData.items.filter((item) => {
const normalisedTitle = normalise(item.title);
return !normalisedTitle.includes(normalisedSearchTerm);
});
if (failingItems.length > 0) {
const failingTitles = failingItems
.map((item) => `- ${item.title}`)
.join('\n');
throw new Error(
`The following titles do not contain the search term "${searchTerm}":\n${failingTitles}`
);
}
};
export const exampleDataOutputsFromResponse = (responseData) => {
if (!responseData.items || responseData.items.length === 0) {
testEvolve.log.warn("No articles found in response.");
return;
}
if (responseData.items[0]) {
testEvolve.log.info('Extract the first object: ', responseData.items[0]);
testEvolve.log.info('Extract the "start_year" of the first object: ', responseData.items[0]['start_year']);
testEvolve.log.info('Extract the first "holding_type" of the first object: ', responseData.items[0]['holding_type'][0]);
}
if (responseData.items[1]) {
testEvolve.log.info('Extract the second object: ', responseData.items[1]);
}
if (responseData.items[2]) {
testEvolve.log.info('Extract the "url" of the third object: ', responseData.items[2]['url']);
testEvolve.log.info('Extract the second "holding_type" of the third object: ', responseData.items[2]['holding_type'][1]);
}
};
features/support/api_helper.rb
module APIHelper
require 'rest-client'
require 'json'
class << self
def find_articles(search_term: "new orleans")
@headers = {
accept: 'application/json',
content_type: 'application/json'
}
url = "#{"https://chroniclingamerica.loc.gov/search/titles/results/?terms="}#{search_term}&format=json"
begin
response = RestClient.get(url, @headers)
data = JSON.parse(response.body)
TE.log.info("API RESPONSE CODE: #{response.code}")
TE.log.info("API RESPONSE BODY: #{data}")
return data
rescue RestClient::ExceptionWithResponse => e
TE.log.error("API Request Failed: #{e.response}")
raise "API Request Failed: #{e.response}"
rescue StandardError => e
TE.log.error("Unexpected Error: #{e.message}")
raise "Unexpected Error: #{e.message}"
end
end
def assert_results_match(response_data, search_term)
return unless response_data["items"].is_a?(Array)
normalised_search_term = normalise(search_term)
failing_items = response_data["items"].reject do |item|
normalise(item["title"]).include?(normalised_search_term)
end
unless failing_items.empty?
failing_titles = failing_items.map { |item| "- #{item['title']}" }.join("\n")
raise "The following titles do not contain the search term '#{search_term}':\n#{failing_titles}"
end
end
def example_data_outputs_from_response(response_data)
return TE.log.warn("No articles found in response.") if response_data["items"].nil? || response_data["items"].empty?
if response_data["items"][0]
TE.log.info("Extract the first object: #{response_data["items"][0]}")
TE.log.info("Extract the 'start_year' of the first object: #{response_data["items"][0]["start_year"]}")
TE.log.info("Extract the first 'holding_type' of the first object: #{response_data["items"][0]["holding_type"][0]}")
end
if response_data["items"][1]
TE.log.info("Extract the second object: #{response_data["items"][1]}")
end
if response_data["items"][2]
TE.log.info("Extract the 'url' of the third object: #{response_data["items"][2]["url"]}")
TE.log.info("Extract the second 'holding_type' of the third object: #{response_data["items"][2]["holding_type"][1]}")
end
end
def normalise(str)
str.downcase.gsub(/[^a-z0-9\s]/, '').strip
end
end
end
- Run your tests.
- TypeScript
- JavaScript
- Ruby
npm run test:ts -- --tags "@example_api_test"
npm run test -- --tags "@example_api_test"
cucumber features -o -t '@example_api_test'