unit testing express middleware
DESCRIPTION
Even for JavaScript software developers well-versed in Agile practices, using test-driven development in Node.js and Express can be challenging. In this presentation, I identify solutions to some of the most significant challenges to using TDD with Express, including mocking data in MongoDB / Mongoose, using promises to control asynchronous testing in Mocha with Chai, and separating concerns to write robust and enduring test suites.TRANSCRIPT
![Page 1: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/1.jpg)
UNIT TESTING EXPRESS MIDDLEWARE
By Morris Singer
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
express mocha+
![Page 2: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/2.jpg)
ABOUT ME• Senior Software Engineer
Cengage Learning
• Expertise:
• Sencha Touch
• Angular.js and Node.js
• Cordova / PhoneGap
• Ruby on Rails
![Page 3: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/3.jpg)
AGENDA• Define Express Middleware and why it isn’t just
a fancy term for controllers or endpoints.
• Review behavior-driven development principles for unit testing.
• Argue why Express Middleware are behavioral units.
• Summarize common challenges testing behavior in Express.
• Review Promises with the Q.js library.
• Learn and implement a pattern for Promise-based Express Middleware.
• Build tests for common scenarios using Mocha, Chai, Chai as Promised, and Mockgoose.
• Answer questions. (10 minutes)
![Page 4: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/4.jpg)
EXPRESS MIDDLEWAREBuilding Your Product, One Layer at a Time
![Page 5: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/5.jpg)
A SIMPLE CASEOne Middleware Per Endpoint
app.get('hello.txt', function (req, res, next) { res.send(200, 'Hello World!'); });
“Why is it called ‘Middleware’ anyway?”
![Page 6: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/6.jpg)
MORE COMPLEX CASESTwo Ways of Stacking Middleware
app.get('hello.txt', function (req, res, next) { req.message = 'Hello World!'; next(); }, function (req, res, next) { res.send(200, req.message); } );
app.get('hello.txt', function (req, res, next) { req.message = 'Hello World!'; next(); }); !app.get('hello.txt', function (req, res, next) { res.send(200, req.message); });
![Page 7: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/7.jpg)
function (req, res, next) { req.message = 'Hello World!'; next(); },
function (req, res, next) { res.send(200, req.message); }
);
app.get('hello.txt',
THE MIDDLEWARE STACK
GET
res.send
generateMessage
sendMessage
![Page 8: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/8.jpg)
TEST BEHAVIOR
![Page 9: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/9.jpg)
MIDDLEWARE IS BEHAVIOR
Middleware:
• Define reusable components.
• Create, modify, and store public variables.
• Send responses to clients.
• Comprise node packages.
![Page 10: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/10.jpg)
COMMON CHALLENGESOr, Why Back End Node Developers Often Avoid TDD
![Page 11: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/11.jpg)
HTTP RESPONSE TESTS
it('should return a 500 error', function (done){ request({ method: 'POST', url: 'http://localhost:3000/api/endpoint' }, function (error, response, body){ expect(response.statusCode).to.equal(500); done(); }); });
What happens when we add a middleware to the stack?
![Page 12: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/12.jpg)
TESTING MID-STACKapp.get('hello.txt', function (req, res, next) { req.message = 'Hello World!'; next(); }); !app.get('hello.txt', function (req, res, next) { res.send(200, req.message); });
How do we pull out these anonymous functions?
![Page 13: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/13.jpg)
ILLUMINATING TEST FAILURESvar httpMocks = require('node-mocks-http'); !it('should call next()', function (done){ var req = httpMocks.createRequest(), res = httpMocks.createResponse(); ! middleware(req, res, function () { done(); }); });
What happens if next() is not called?
![Page 14: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/14.jpg)
KNOWING WHEN TO TEST
var httpMocks = require('node-mocks-http'); !it('should call next()', function (done){ var req = httpMocks.createRequest(), res = httpMocks.createResponse(); ! middleware(req, res); ! expect(req.foo).to.equal('bar'); });
When is the assertion run?
![Page 15: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/15.jpg)
TESTING WITH DATA
app.get('path/to/post', function (req, res, next) { Post.findOne(params).exec(function (err, post){ res.json(200, post); }); });
Where do data come from?
![Page 16: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/16.jpg)
DEALING WITH POLLUTION
How does one reset the data?
it('should update the first post', function (){ /* ... */ }); !it('should get the first post', function (){ /* ... */ });
![Page 17: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/17.jpg)
MOCKING DEPENDENCIESapp.get('endpoint', function (req, res, next) { request({ method: 'GET', url: 'http://example.com/api/call' }, function (error, response, body) { req.externalData = body; next(); }); });
How does one cut out the external data source?
![Page 18: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/18.jpg)
WRITING ROBUST TESTS
What if someone adds a middleware?
app.get('hello.txt', function (req, res, next) { req.message = 'Hello World!'; next(); }); !app.get('hello.txt', function (req, res, next) { res.send(200, req.message); });
![Page 19: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/19.jpg)
PROMISESLinks in a Chain of Async Operations
![Page 20: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/20.jpg)
queryDatabase(params, function (result) { makeRequestOfThirdPartyService(result, function (result) { writeFile(result, function (handle) { sendFileOverHttp(handle, function (result) { }, function (err) { // Handle Error }); }, function (err) { // Handle Error }); }, function (err) { // Handle Error }); });
PYRAMID OF DOOM
![Page 21: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/21.jpg)
PROMISES TO THE RESCUE
queryDatabase() .then(makeRequestOfThirdPartyService) .then(updateDatabase) .then(writeFile) .then(sendFileOverHttp) .catch(function (err) { // Handle Errors }).done();
![Page 22: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/22.jpg)
WHAT IS A PROMISEA promise is:
• a delegate
• for an asynchronous action
• that:
• collects references to callbacks
• maintains state, and
• provides a mechanism for chaining.
![Page 23: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/23.jpg)
THEN, CATCH, FINALLY, DONE
myPromise() .then(function (result) { ! }) .catch(function (err) { ! }) .finally(function () { ! }) .done();
Data sent, received, read, written, etc.
Problems
No matter what
![Page 24: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/24.jpg)
THE FLIP SIDEvar Q = require('q'); !function myPromise() { var deferred = Q.defer(); ! if (conditionX) { ! deferred.resolve('Result'); ! } else { ! deferred.reject(new Error()); ! } ! return deferred.promise; }
Triggers then(). Passes ‘Result’
Triggers catch(). Passes new Error()
![Page 25: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/25.jpg)
PUTTING IT ALL TOGETHER
var Q = require('q'); !function myPromise() { var deferred = Q.defer(); ! if (conditionX) { ! ! } else { ! ! } ! return deferred.promise; }
myPromise() ! .then(function (result) { ! }) ! .catch(function (err) { ! }) ! .finally(function () { ! }) ! .done();
deferred.resolve(‘Result’);
deferred.reject(new Error());
![Page 26: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/26.jpg)
THE LIFE OF A PROMISE
Pending
Fulfilled
Rejected
then() finally()
catch() finally()
![Page 27: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/27.jpg)
THE PROMISE CHAIN
.then()
.catch()
.finally()
Q.defer().promise
Q.when()
Q.promise()
Q.fcall()
.done()
Start a new promise chain Continue the chain End the chain
Return a promise
![Page 28: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/28.jpg)
IN PRACTICE
Q.promise()
Promise A Promise C Promise E
Promise B Promise D Promise F
.then().then().then().catch().finally().done()
![Page 29: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/29.jpg)
DO NOT BREAK YOUR CHAINSOtherwise, your user may be left hanging…
![Page 30: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/30.jpg)
NOT BREAKING CHAINSvar Q = require('q'); !Q.when(function () {}) .then(function (result) { var deferred = Q.defer(); ! /* Do async and call deferred.resolve() and deferred.reject(). */ ! return deferred.promise; }) .then(function (result) { var deferred = Q.defer(); ! /* Do async and call deferred.resolve() and deferred.reject(). */ ! return deferred.promise; }) .catch(function (err) { ! }) .done();
Resolving here
calls the referenced function, passing the result as an argument.
Rejections of either promise result in the referenced function called with err
and uncaught rejections are thrown as errors here.
![Page 31: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/31.jpg)
Always return a promise or call done(). Period.
![Page 32: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/32.jpg)
EXPRESS + QThe “Eureka” Moment
![Page 33: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/33.jpg)
OVERVIEW
• Pull middleware into endpoints and tests.
• Mock req and res.
• Use promises as link between middleware and endpoints.
• Return client-server interaction to endpoint.
• Use promises with Mocha.
![Page 34: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/34.jpg)
PULL MIDDLEWARE INTO ENDPOINTS, TESTS
Endpoint
Test
MiddlewareMiddleware
MiddlewareMiddleware
Endpoint
TestTest
! !
Old Paradigm
" !
New Paradigm
![Page 35: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/35.jpg)
PULL MIDDLEWARE INTO ENDPOINTS, TESTS
app.get('example/uri', function (req, res, next) { /* Middleware implementation */ }, function (req, res, next) { /* Middleware implementation */ });
var middleware = { first: function (req, res, next) {}, second: function (req, res, next) {} }; app.get('example/uri', middleware.first, middleware.second);
! !
Old Paradigm
" !
New Paradigm
![Page 36: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/36.jpg)
MOCK REQ, RES
• We need a way to call our middleware functions directly.
• Our middleware functions expect req and res to be passed as arguments.
• So, we mock req and res.
module npm
node-mocks-http https://www.npmjs.org/package/node-mocks-http
express-mocks-http https://www.npmjs.org/package/express-mocks-http
![Page 37: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/37.jpg)
MOCK REQ, RESit ('should do something', function (done) { var requestParams = { uri: 'http://path.to/endpoint', method: 'POST' }; ! request(requestParams, function (error, response, body) { expect(response.body).to.equal( /* Expected Data */ ); done(); }); });
it ('resolves under condition X with result Y', function () { ! var req = httpMocks.createRequest(), res = httpMocks.createResponse(); ! /* Call middleware(req, res) and assert. */ !});
! !
Old Paradigm
" !
New Paradigm
![Page 38: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/38.jpg)
USE PROMISES AS LINK BETWEEN MIDDLEWARE AND ENDPOINTS
• Clean, standardized interface between asynchronous middleware and endpoints.
• Both endpoints and tests can leverage the same mechanism in the middleware for serializing logic.
then
![Page 39: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/39.jpg)
USE PROMISES AS LINK BETWEEN MIDDLEWARE AND ENDPOINTS
module.exports = function (req, res, next) { ! /* Define middleware behavior and call res.json(), next(), etc. */ };
var Q = require('q'); module.exports = function (req, res) { var deferred = Q.defer(); /* Define middleware behavior and resolve or reject promise. */ return deferred.promise; };
! !
Old Paradigm
" !
New Paradigm
![Page 40: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/40.jpg)
RETURN CLIENT-SERVER INTERACTION TO ENDPOINT
Endpoint
Res
Req
Middleware
Res
ReqClient
Endpoint
Res
Req
Middleware
Res
ReqClient
! !
Old Paradigm
" !
New Paradigm
![Page 41: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/41.jpg)
RETURN CLIENT-SERVER INTERACTION TO ENDPOINT
var middleware = { first: function (req, res, next) {}, second: function (req, res, next) {} }; app.get('example/uri', middleware.first, middleware.second);
var middleware = require('./middleware.js'); app.get('example/uri', function (req, res, next) { middleware.first(req, res) .then(function () { next(); }) .catch(res.json) .done(); }, middleware.second(req, res) .then(function () { next(); }) .catch(res.json) .done(); });
! !
Old Paradigm
" !
New Paradigm
![Page 42: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/42.jpg)
USING PROMISES WITH MOCHA (CHAI-AS-PROMISED)We need:
• A test framework syntax that facilitates easy async testing. (Supported natively in Mocha since 1.18.0)
• An assertion syntax that we are familiar with. (Chai)
• A set of assertions that facilitate easily writing tests of promises. (Chai-As-Promised) then
mocha
![Page 43: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/43.jpg)
USING PROMISES WITH MOCHA (CHAI-AS-PROMISED)
describe('middleware', function () { it ('resolves under condition X with result Y', function () { ! var req = httpMocks.createRequest(), res = httpMocks.createResponse(); ! middleware(req, res).then(function (done) { /* Assert here. */ }).finally(done).done(); ! });
! !
Old Paradigm
" !
New Paradigm
describe('middleware', function () { it ('resolves under condition X with result Y', function () { ! var req = httpMocks.createRequest(), res = httpMocks.createResponse(); ! return expect(middleware(req, res)).to.eventually.equal('value'); ! });
![Page 44: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/44.jpg)
THE NEW PARADIGMLooking at the Whole Picture
![Page 45: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/45.jpg)
Return Client-Server Interaction to Endpoints
ENDPOINTS
Pull Middleware into Endpoint
var middleware = require('./middleware.js'); app.get('example/uri', function (req, res, next) { middleware(req, res) .then(function () { next(); }) .catch(res.json) .done(); });
![Page 46: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/46.jpg)
Use Promise as Link Between Middleware and Endpoints
MIDDLEWARE
var Q = require('q'); module.exports = function (req, res) { var deferred = Q.defer(); /* Define middleware behavior and resolve or reject promise. */ return deferred.promise; };
![Page 47: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/47.jpg)
Pull Middleware Into Tests
TESTUse Promises with Mocha
var httpMocks = require('node-mocks-http'), chai = require('chai'), chaiAsPromised = require('chai-as-promised'); chai.use(chaiAsPromised);
Mock Req, Res
var middleware = require('path/to/middleware');
var req, res; beforeEach(function (done) { req = httpMocks.createRequest(), res = httpMocks.createResponse(); });
describe('middleware', function () { it ('resolves under condition X with result Y', function () { return expect(middleware(req, res)).to.be.fulfilled.then(function () { /* Assert */ }); }); it ('rejects under condition X with error Y', function () { return expect(middleware(req, res)).to.be.rejectedWith('Error String'); }); });
![Page 48: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/48.jpg)
TESTING WITH DATAMocking Mongoose and Using Fixtures to Build a Robust
and Effective Test Suite
![Page 49: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/49.jpg)
THE PROBLEM WITH DATA
We need a solution where:
• Testing does not depend on the environment,
• Data travel with the repo,
• The database can easily be reset to an initial data set.
![Page 50: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/50.jpg)
(MONGODB + MONGOOSE)*
* Solutions are available for other setups. You can also roll your own, without too much heartache.
![Page 51: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/51.jpg)
THE HIGH LEVEL• Mock MongoDB with in-memory database that can be
reset between tests and thrown away after tests run. (Mockgoose)
• Write test data as code that can move with the repo. (Fixtures)
• Build test harness that loads fixtures into mock database before tests run.
![Page 52: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/52.jpg)
MOCKING MONGOOSE
var mongoose = require('mongoose'); var mockgoose = require('mockgoose'); mockgoose(mongoose);
![Page 53: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/53.jpg)
CODING DATA IN FIXTURES
module.exports.User = [ { name: 'Maeby' }, { name: 'George Michael' } ];
![Page 54: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/54.jpg)
LOADING FIXTURES
var loader = require('pow-mongoose-fixtures'); !var users = require('users.js'); /* User fixtures */ !beforeEach(function (done) { loader.load(users); done(); }); !/* Build Tests Here */
![Page 55: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/55.jpg)
TDD EXERCISESUse TDD in Pairs to Complete the Accompanying TDD / Express Exercises
![Page 56: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/56.jpg)
QUESTIONS
![Page 57: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/57.jpg)
IMPROVEMENTS?
• There are still some shortcomings in this approach, though it is better than other approaches I have seen.
• Particularly, there are still some failure modes that will just timeout.
• If you can improve on this pattern, PLEASE let me know!
![Page 58: Unit Testing Express Middleware](https://reader033.vdocuments.mx/reader033/viewer/2022052412/557e5a52d8b42af46d8b5361/html5/thumbnails/58.jpg)
GET IN TOUCH
# @morrissinger
$ linkedin.com/in/morrissinger
% morrissinger.com
& github.com/morrissinger