Skip to content

Instantly share code, notes, and snippets.

@bennettscience
Last active February 24, 2021 02:45
Show Gist options
  • Save bennettscience/e405c0f70e633c71e9063fdc0d98bf38 to your computer and use it in GitHub Desktop.
Save bennettscience/e405c0f70e633c71e9063fdc0d98bf38 to your computer and use it in GitHub Desktop.
A simple Mocking framework in Google Apps Script
/**
* Fixtures provide a conventient way to define expected data strucures from API calls. This
* works based on JSON keys and gives a flexible, namespaced access for quickly making Mocks.
*/
const fixtures = (function() {
const init = function() {
return this;
}
// Can be generic method requests, status codes, or mirror your target endpoints for library creation.
const requests = {
"get": {
"method": "ANY",
"endpoint": "400",
"data": "Hello world!",
"status_code": 200
},
"401_invalid_access_token": {
"method": "ANY",
"endpoint": "401_invalid_access_token",
"data": {},
"headers": {
"WWW-Authenticate": "Bearer realm=\"canvas-lms\""
},
"status_code": 401
},
"401_unauthorized": {
"method": "ANY",
"endpoint": "401_unauthorized",
"data": {},
"status_code": 401
},
// ...add more
return {
requests: requests,
}
})();
/**
* A Mock class accepts a fixture and assumes the expected data structure in the test. The `fetch`
* method allows for mocked access to the Apps Script UrlFetchApp class so tests can be written
* as if they were in production.
*/
class Mock {
constructor(params) {
for(var name in params) {
Object.defineProperty(this, name, {
value: params[name],
enumerable: true,
configurable: true
})
}
}
fetch() {
return {
getContentText: () => this.data,
getResponseCode: () => Number(this.status_code)
}
}
}
/**
* Factory to create Mock objects.
* @param {string} requirement Fixture namespace to access
* @param {string} endpoint Specific key within the fixture
*/
function registerMock(requirement, endpoint) {
const f = fixtures.init();
let mock = new Mock(f[requirement][endpoint])
console.log(mock instanceof Mock)
return mock
}
/**
* This class is specific to working with the CanvasLMS API, but it could be generalized for
* any kind of API work. When Canvas-specific objects are created, a Requester instance is attached
* as an attribute and it handles calls from the Canvas object directly. The user won't really
* interact with Requester unless they're building a custom call.
*
* Using dependency injection, requests can be mocked in unit tests (example below). UrlFetchApp,
* if defined at instantiation, can be a Mock object which will return expected results without
* requiring major modifications to Requester.
*
* @param {string} baseUrl The TLD to request
* @param {string} accessToken Valid API key
* @param {object} UrlFetchApp_ Inject a Mock class for unit testing
*/
class Requester {
constructor(baseUrl, accessToken, {UrlFetchApp_=UrlFetchApp}={}) {
this._baseUrl = baseUrl;
this._accessToken = accessToken;
this.UrlFetchApp = UrlFetchApp_;
}
get_request(url, headers, params=null) {
const opts = {
"method": "GET",
"contentType": "application/json",
"headers": headers,
"muteHttpExceptions": true,
}
return this.UrlFetchApp.fetch(url, opts);
}
/**
* @param {string} method HTTP protocol
* @param {string} endpoint API resource
* @param {object} headers Optional headers to include
* @param {object} payload Optional data to send to the API
*
* @returns {object} response
*/
request(method, endpoint, headers=null, payload={}) {
let req_method, response;
method = method.toUpperCase();
const full_url = `${this._baseUrl}/api/v1/${endpoint}`;
if (!headers) {
headers = {
"Authorization": "Bearer " + this._accessToken
}
}
if(method === "GET") {
req_method = this.get_request.bind(this);
} else {
throw new Error(`We're not ready to do more yet.`);
}
response = req_method(full_url, headers, payload)
return response.getContentText();
}
}
/**
* Requester.request() can be tested using a mock by injecting the Mock object.
*/
function test() {
let M = register_uris("requests", "get");
let requester = new Requester("https://www.example.com", "abc123", {UrlFetchApp_: Mock});
let resp = Requester.request("GET", "foo/bar");
console.log(resp) // => "Hello World"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment