Skip to content

Instantly share code, notes, and snippets.

@bryan-c-oconnell
Last active October 4, 2016 21:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bryan-c-oconnell/6f5ec5d2c4b17f1ceb9e to your computer and use it in GitHub Desktop.
Save bryan-c-oconnell/6f5ec5d2c4b17f1ceb9e to your computer and use it in GitHub Desktop.
bryanoconnell.blogspot.com - Exploring TypeScript (an API testing module)
module BOC.ApiTester {
export class ApiTests {
private results: ResultsLog;
private timeTracker: Timer;
private outputElement: HTMLElement;
static baseUrl: string = "http://jsonplaceholder.typicode.com";
constructor(elementIdToDisplayResults: string) {
this.results = new ResultsLog();
this.timeTracker = new Timer();
this.outputElement = document.getElementById(elementIdToDisplayResults);
this.outputElement.innerHTML = ""; // Reset to remove any previous results.
}
public run(): void {
this.timeTracker.start();
try {
// GET examples - Get all Users, and pick a random User for further tests:
var allUserRecords: JSON = this.apiCall("GET",(ApiTests.baseUrl + "/users"));
var randomUser: number = this.getRandomNumberInRange(0,(Object.keys(allUserRecords).length - 1));
var singleUser = allUserRecords[randomUser];
// POST example - assign a new Todo to the User:
var newToDo: string = JSON.stringify({
userId: singleUser.id,
title: "Please delete the last post you made; it is no longer needed.",
completed: false
});
var createNewToDo: JSON = this.apiCall("POST",(ApiTests.baseUrl + "/todos"), newToDo);
// DELETE example - complete the ToDo that was assigned to the user by deleting their last Post:
var allPostsForUser: JSON = this.apiCall("GET",(ApiTests.baseUrl + "/posts?userId=" + singleUser.id));
var lastPost = allPostsForUser[(Object.keys(allPostsForUser).length - 1)];
var deleteLastPost: JSON = this.apiCall("DELETE",(ApiTests.baseUrl + "/posts/" + lastPost.id));
// PUT example - User has accomplished the requested task, so let's update the 'completed' status of one of their open ToDos.
// NOTE: This API does not actually save changes, so we can't update the record we "created" in the POST example.
var incompleteToDosForUser: JSON = this.apiCall("GET",(ApiTests.baseUrl + "/todos?completed=false&userId=" + singleUser.id));
var randomToDo: number = this.getRandomNumberInRange(0,(Object.keys(incompleteToDosForUser).length - 1));
var updatedToDo = incompleteToDosForUser[randomToDo];
updatedToDo.completed = true;
var completedToDo: JSON = this.apiCall("PUT",(ApiTests.baseUrl + "/todos/" + updatedToDo.id), JSON.stringify(updatedToDo));
}
catch (error) {
this.results.addEntry("Error occurred: " + error + "; aborting test execution process.");
}
finally {
this.timeTracker.stop();
}
}
public displayResults(): void {
this.outputElement.innerHTML += this.results.getResultsAsHtml();
this.outputElement.innerHTML += this.timeTracker.getRuntimeAsHtml();
}
private apiCall(action: string, endpointUrl: string, contents: string = ""): JSON {
var requestObject: ApiRequest = new ApiRequest(action, endpointUrl);
var response: string = requestObject.makeCall(contents);
if (requestObject.requestSucceeded) {
this.results.addEntry(action + " - " + endpointUrl + " - PASS");
this.results.addEntry(response);
return JSON.parse(response);
}
else {
throw (action + " - " + endpointUrl + " failed to return the expected response.");
}
}
// Returns a random number between min (inclusive) and max (exclusive)
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random
private getRandomNumberInRange(min: number, max: number): number {
return Math.floor(Math.random() * (max - min)) + min;
}
}
export class ResultsLog {
private ListOfResults: Array<string>;
constructor() {
this.reset();
}
public addEntry(entry: string): void {
this.ListOfResults.push(entry);
}
public getResultsAsHtml() {
var i: number = 0;
var entry: string = "";
var resultsHtml: string = "";
while (entry = this.ListOfResults[i++]) {
resultsHtml += "<p>" + entry + "</p>"
}
return resultsHtml;
}
public reset(): void {
this.ListOfResults = [];
}
}
export class Timer {
private timerIsRunning: boolean;
private startTime: string;
private endTime: string;
private totalRuntime: number;
constructor() {
this.timerIsRunning = false;
this.resetTimeData();
}
private resetTimeData(): void {
this.totalRuntime = 0;
var currentTime: string = new Date().toLocaleString();
this.startTime = currentTime;
this.endTime = currentTime;
}
public start(): void {
if (!this.timerIsRunning) {
this.timerIsRunning = true;
this.resetTimeData();
}
}
public stop(): void {
if (this.timerIsRunning) {
this.timerIsRunning = false;
this.endTime = new Date().toLocaleString();
this.calculateTotalRuntime();
}
}
private calculateTotalRuntime(): void {
var begin: number = new Date(this.startTime).getTime();
var end: number = new Date(this.endTime).getTime();
this.totalRuntime = end - begin;
}
public getRuntimeAsHtml(): string {
var runtimeInMilliseconds: number = this.totalRuntime;
// Calcuates the number of minutes the Timer ran for, and removes that from total runtime.
var minutes: number = Math.floor(runtimeInMilliseconds / 1000 / 60);
runtimeInMilliseconds -= (minutes * 1000 * 60);
// Calculates the number of remaining seconds the Timer ran for AFTER removing the number of minutes from total runtime.
var seconds: number = Math.floor(runtimeInMilliseconds / 1000);
var runtimeMessage: string = "<p>Total runtime: " + minutes.toString() + " minutes, " + seconds.toString() + " seconds.</p>";
return runtimeMessage;
}
}
export class ApiRequest {
private actionType: string;
private urlToCall: string;
private responseData: string;
private _requestSucceeded: boolean;
constructor(method: string, targetUrl: string) {
this.actionType = method;
this.urlToCall = targetUrl;
this._requestSucceeded = false;
}
private createRequestObject(): XMLHttpRequest {
var xhr: XMLHttpRequest = new XMLHttpRequest();
// The tests need to run in the specified order, because some steps create or collect data that later steps depend on.
// Setting the last parameter here to 'true' will cause asynchronous calls to occur, which will interrupt that order.
xhr.open(this.actionType, this.urlToCall, false);
// Tells the server that the call was made for AJAX purposes.
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
// Circumvents the browser's desire to cache duplicate GET calls.
xhr.setRequestHeader("If-Modified-Since", "Sun, 6 Jun 1980 00:00:00 GMT");
// Asks to get data back as JSON.
xhr.setRequestHeader("Content-Type", "application/json");
return xhr;
}
public makeCall(bodyContents: string = ""): string {
var xhr: XMLHttpRequest = this.createRequestObject();
xhr.onerror = (err) => {
this.responseData = "Error occurred calling " + this.urlToCall + ". Message: " + err.message;
this._requestSucceeded = false;
}
// http://stackoverflow.com/questions/12756423/is-there-an-alias-for-this-in-typescript - #3
xhr.onload = () => {
if (this.requestWasSuccessful(xhr.status)) {
this.responseData = xhr.responseText;
this._requestSucceeded = true;
}
else {
this.responseData = "API responded with an error status: " + xhr.status.toString() + " - " + xhr.statusText;
this._requestSucceeded = false;
}
};
xhr.send(bodyContents);
return this.responseData;
}
// http://www.restapitutorial.com/httpstatuscodes.html
private requestWasSuccessful(statusCode: number): boolean {
return (((statusCode > 99) && (statusCode < 299)) ? true : false);
}
get requestSucceeded(): boolean {
return this._requestSucceeded;
}
}
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>TypeScript API Tester</title>
<link rel="stylesheet" href="app.css" type="text/css" />
<script src="api-tester.js"></script>
</head>
<body>
<h1>TypeScript API Tester</h1>
<p>
<input type="button" value="Run Tests" onclick="runTests()" />
</p>
<div id="displayResults"></div>
</body>
</html>
<script type="text/javascript">
function runTests() {
var myTests = new BOC.ApiTester.ApiTests("displayResults");
alert("Running tests ...")
myTests.run();
myTests.displayResults();
alert("Test process complete.");
}
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment