Skip to content

Commit

Permalink
Add the mock server feature
Browse files Browse the repository at this point in the history
  • Loading branch information
berniegp committed Jan 10, 2019
1 parent ecfd352 commit 2209091
Show file tree
Hide file tree
Showing 8 changed files with 805 additions and 74 deletions.
6 changes: 6 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,18 @@ module.exports = {
// Saving 2 characters is not worth the potential errors
'curly': 'error',

// A chain of 'if' and 'else if' statements is clearer than multiple individual 'if' blocks
'no-else-return': [ 'error', { allowElseIf: true } ],

// Finding good names is hard so allow reuse
'no-param-reassign': 0,

// Increment with += 1 is just too long to type
'no-plusplus': 0,

// Finding good names is hard so allow reuse
'no-shadow': 0,

// This is still the best way to express the private api intent
'no-underscore-dangle': ['error', { 'allowAfterThis': true }],

Expand Down
15 changes: 6 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Contributing to mock-xmlhttprequest

## How to contribute code

* Login to GitHub (you need an account)
* Open an issue in the [issue tracker](https://github.com/berniegp/mock-xmlhttprequest/issues)
* Fork the main repository from [GitHub](http://github.com/berniegp/mock-xmlhttprequest)
Expand All @@ -11,23 +10,21 @@
* Open a [pull request](https://github.com/berniegp/mock-xmlhttprequest/pulls)

### Branch contents

Please organize the commits in your branches logically. Use squash to combine multiple commits, split bigger changes in multiple commits (or pull requests) when relevant, etc. The general idea is to make it easier for a reviewer to inspect your changes and accept them.

If you are familiar enough with Git to do this, make sure your branch is *rebased* on the target branch (usually *master*) and the commit history is clean so pull requests can be merged with a *fast-forward* merge.

### Runing the unit tests

$ npm test

All tests must pass and the included lint step must be successful.

## Coding style

The coding style is based on the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript/tree/es5-deprecated/es5).
The coding style is based on the [Airbnb JavaScript Style Guide](https://github.com/airbnb/javascript) with few modifications.

In general:
* Be consistent with the existing coding style
* Avoid overly "clever" code unless there's a compelling reason for it
* Don't be afraid to comment the code and the reasons behind it
* Use white space
* Be consistent with the existing coding style.
* Avoid overly "clever" code unless there's a compelling reason for it.
* Don't be afraid to comment the code and the reasons behind it.
* Use white space.
247 changes: 185 additions & 62 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,25 @@
# mock-xmlhttprequest
XMLHttpRequest mock for testing

Based on the [XMLHTTPRequest specification](https://xhr.spec.whatwg.org), version '28 November 2018'
## Table of Contents
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Low-Level Quick Start](#low-level-quick-start)
- [Features](#features)
- [Supported](#supported)
- [Not supported](#not-supported)
- [Usage](#usage)
- [Mock Server](#mock-server)
- [Basic Setup](#basic-setup)
- [Routes](#routes)
- [HTTP Request Method](#http-request-method)
- [Request URL Matcher](#request-url-matcher)
- [Request Handler](#request-handler)
- [Mock response methods](#mock-response-methods)
- [Hooks](#hooks)
- [Run Unit Tests](#run-unit-tests)
- [Contributing](#contributing)
- [License](#license)

## Installation
via [npm (node package manager)](https://github.com/npm/npm)
Expand All @@ -13,31 +31,59 @@ via [npm (node package manager)](https://github.com/npm/npm)
## Quick Start
```javascript
const assert = require('assert');
const MockXMLHttpRequest = require('mock-xmlhttprequest').newMockXhr();
const MockXMLHttpRequest = require('mock-xmlhttprequest');

// Install the server's XMLHttpRequest mock in the "global" context.
// "new XMLHttpRequest()" will then create a mock request to which the server will reply.
const server = MockXMLHttpRequest.newServer({
get: ['/my/url', {
// status: 200 is the default
headers: { 'Content-Type': 'application/json' },
body: '{ "message": "Success!" }',
}],
}).install(global);

// Do something that send()s an XMLHttpRequest to '/my/url'
const result = MyModuleUsingXhr.someAjaxMethod();

// Assuming someAjaxMethod() returns the parsed JSON body
assert.equal(result.message, 'Success!');

// Install in global context so "new XMLHttpRequest()" works in MyModuleUsingXhr
global.XMLHttpRequest = MockXMLHttpRequest;
// Restore the original XMLHttpRequest from the context given to install()
server.remove();
```

## Low-Level Quick Start
An alternative usage pattern not using the mock server based only on the `MockXhr` class. Mostly here for historical reasons because it predates the mock server.

const MyModuleUsingXhr = require('./MyModuleUsingXhr');
```javascript
const assert = require('assert');
const MockXMLHttpRequest = require('mock-xmlhttprequest');
const MockXhr = MockXMLHttpRequest.newMockXhr();

// Mock JSON response
MockXMLHttpRequest.onSend = (xhr) => {
const response = {
result: 'success',
};
const responseHeaders = {
'Content-Type': 'application/json',
}l
xhr.respond(200, responseHeaders, JSON.stringify(response));
MockXhr.onSend = (xhr) => {
const responseHeaders = { 'Content-Type': 'application/json' };
const response = '{ "message": "Success!" }';
xhr.respond(200, responseHeaders, response);
};

// Method under test that uses XMLHttpRequest
// Install in the global context so "new XMLHttpRequest()" uses the XMLHttpRequest mock
global.XMLHttpRequest = MockXhr;

// Do something that send()s an XMLHttpRequest to '/my/url'
const result = MyModuleUsingXhr.someAjaxMethod();

// assuming someAjaxMethod() returns the value of the 'result' property
assert.equal(result, 'success');
// Assuming someAjaxMethod() returns the value of the 'result' property
assert.equal(result.message, 'Success!');

// Remove the mock class from the global context
delete global.XMLHttpRequest;
```

## Features
Based on the [XMLHTTPRequest specification](https://xhr.spec.whatwg.org), version '28 November 2018'.

### Supported
- events and states
- `open()`, `setRequestHeader()`, `send()` and `abort()`
Expand All @@ -50,71 +96,105 @@ assert.equal(result, 'success');
- `removeEventListener()` not implemented (https://dom.spec.whatwg.org/#dom-eventtarget-removeeventlistener)
- `dispatchEvent()` does not return a result. (https://dom.spec.whatwg.org/#dom-eventtarget-dispatchevent)
- synchronous requests (`async` == false)
- parsing the url and setting the `username` and `password`
- parsing the URL and setting the `username` and `password`
- the timeout attribute (call `MockXhr.setRequestTimeout()` to trigger a timeout)
- `withCredentials`
- `responseUrl` (the final request url with redirects)
- `responseUrl` (i.e. the final request URL with redirects) is not automatically set. This can be emulated in a request handler.
- Setting `responseType` (only the empty string responseType is used)
- `overrideMimeType`
- `responseXml`

## Usage

### Unit Test Setup
```javascript
// MyModuleTest.js
const MockXMLHttpRequest = require('mock-xmlhttprequest');
### Mock Server
The mock server is the easiest way to define responses for one or more requests. Handlers can be registered for any HTTP method and URL without having to dig in the lower-level [hooks](INTERNAL) of this library.

// To test code that uses XMLHttpRequest directly with 'new XMLHttpRequest()'
global.XMLHttpRequest = MockXMLHttpRequest.newMockXhr();
#### Basic Setup
The basic structure of tests using the mock server is:

// Tests
// ...
```javascript
const server = require('mock-xmlhttprequest').newServer( /* routes */ );

// Cleanup after the tests
delete global.XMLHttpRequest;
try {
server.install(global);

// Setup routes
// Test
} finally {
server.remove();
}
```

### Hooks
- `install(context)` installs the server's XMLHttpRequest mock in the given context (e.g. `global` or `window`).
- `remove()` reverts what `install()` did.

The hooks defined in this library can be set at these locations:
- On an instance of `MockXMLHttpRequest` (i.e. a mocked `XMLHttpRequest`).
- On a local `XMLHttpRequest` mock returned by `MockXMLHttpRequest.newMockXhr()`.
- Globally, directly on the `MockXMLHttpRequest` object (from `require('mock-xmlhttprequest')`). Note that each call to `require('mock-xmlhttprequest')` in a node process will return the same instance of `MockXMLHttpRequest`. This means that hooks set directly on `MockXMLHttpRequest` need to be removed manually when no longer needed. This method is therefore not recommended.
#### Routes
Routes are defined by these 3 elements:
- An [HTTP request method](https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Request_methods).
- A request URL matcher.
- A request handler.

#### MockXMLHttpRequest.onCreate(xhr)
Called when an instance of `MockXMLHttpRequest` is created. This makes it possible to capture `XMLHttpRequest`s created in the module under test.
When an XMLHttpRequest is sent, the server responds with the request handler of the first route matching the request method and URL. Note that route insertion order is important here. If no route is found for a request, no action is taken.

```javascript
const MockXMLHttpRequest = require('mock-xmlhttprequest');
const LocalXMLHttpRequestMock = MockXMLHttpRequest.newMockXhr();
The route concept is loosely based on the [Express framework](https://expressjs.com/).

// Global hook for all requests from the local mock
LocalXMLHttpRequestMock.onCreate = (xhr) => { /*...*/ };
##### HTTP Request Method
Any `string` with a valid HTTP request method is allowed. This includes standard methods like `GET`, `POST`, `PUT` and `DELETE`, but also other method names as well.

// Global hook for all requests from all mocks
MockXMLHttpRequest.onCreate = (xhr) => { /*...*/ };
##### Request URL Matcher
This can be:
- A `string` (e.g. '/get') in which case it must match exactly the request URL.
- A `RegExp` against which the request URL is tested.
- A `function` (signature `matches(url)`) which must return true if the request URL matches.

##### Request Handler
This can be:
- An `object` with the response properties. The default values are: ` { status: 200, headers: {}, body: null, statusText: 'OK' }`. An empty object is also allowed here to accept all default values.
- A `function` (signature `handler(xhr)`) that calls the [mock response methods](INTERNAL) directly.
- An array of `object` and `function` request handlers. In this case, the first matching request gets the first handler, the second gets the second handler and so on. The last handler is reused if the number of matching requests exceeds the number of handlers in the array.

These handlers are equivalent:
```javascript
const handlerObj = {};
const handlerFn = (xhr) => { xhr.respond(); };
const handlerArray = [{}];
```

#### MockXMLHttpRequest.onSend(xhr)
Called when `XMLHttpRequest.send()` has done its processing and the test case should start using the mock reponse methods. In a real `XMLHttpRequest`, this would be where the actual http request takes place.
Request handlers are invoked in a different call stack (using `setTimeout()`) than the one that called `send()` on the `XMLHttpRequest`. Therefore you will probably need to use your test framework's asynchronous test support (e.g. for Mocha: https://mochajs.org/#asynchronous-code) to complete the unit test.

This callback is invoked in an empty callstack (using `setTimeout()`). You will probably need to use your test framework's asynchronous test support (e.g. for Mocha: https://mochajs.org/#asynchronous-code).
#### MockXMLHttpRequest.newServer(routes = {})
Factory method to create a new server. The optional `routes` parameter allows defining routes directly at construction. Each property name in `routes` corresponds to an HTTP method and its value must be an array containing `[url_matcher, request_handler]`.

Example:
```javascript
const MockXMLHttpRequest = require('mock-xmlhttprequest');
const LocalXMLHttpRequestMock = MockXMLHttpRequest.newMockXhr();
const handlerFn = (xhr) => { xhr.respond(); };
newServer({
get: ['/get', { status: 200 }],
'my-method': ['/my-method', { status: 201 }],
post: ['/post', [handlerFn, { status: 404 }],
});
```

// Global hook for all requests from the local mock
LocalXMLHttpRequestMock.onCreate = (xhr) => { /*...*/ };
#### get(matcher, handler)
Add a [route](INTERNAL) for the `GET` HTTP method.

// Hook local to an instance of MockXMLHttpRequest
const xhr = new LocalXMLHttpRequestMock(); // or, more likely, captured in the onCreate() hook
xhr.onSend = (xhr) => { /*...*/ };
#### post(matcher, handler)
Add a [route](INTERNAL) for the `POST` HTTP method.

// Global hook for all requests from all mocks
MockXMLHttpRequest.onCreate = (xhr) => { /*...*/ };
```
#### put(matcher, handler)
Add a [route](INTERNAL) for the `PUT` HTTP method.

#### delete(matcher, handler)
Add a [route](INTERNAL) for the `DELETE` HTTP method.

#### addHandler(method, matcher, handler)
Add a [route](INTERNAL) for the `method` HTTP method.

#### setDefaultHandler(handler)
Set a default request handler for requests that don't match any route.

#### getRequestLog()
Returns the list of all requests received by the server. Each entry has `{ method, url }`. Can be useful for debugging.

### Mock response methods

Expand All @@ -123,15 +203,15 @@ Fires a request upload progress event where `transmitted` is the number of bytes

May only be called when the request body is not null and the upload is not complete. Can be followed by any other mock response method.

#### respond([status = 200], [headers = {}], [body = null], [statusText = 'OK'])
Complete response method which sets the response headers and body. Will fire the appropriate 'readystatechange', `progress`, `load`, etc. (upload) events. The state of the request will be set to `DONE`.
#### respond(status = 200, headers = {}, body = null, statusText = 'OK')
Complete response method which sets the response headers and body. Will fire the appropriate `readystatechange`, `progress`, `load`, etc. (upload) events. The state of the request will be set to `DONE`.

This is a shorthand for calling `setResponseHeaders()` and `setResponseBody()` in sequence.

No other mock response methods may be called after this one until `open()` is called.

#### setResponseHeaders([status = 200], [headers = {}], [statusText = 'OK'])
Sets the response headers only. Will fire the appropriate 'readystatechange', `progress`, `load`, etc. (upload) events. Will set the request state to `HEADERS_RECEIVED`.
#### setResponseHeaders(status = 200, headers = {}, statusText = 'OK')
Sets the response headers only. Will fire the appropriate `readystatechange`, `progress`, `load`, etc. (upload) events. Will set the request state to `HEADERS_RECEIVED`.

Should be followed by either `downloadProgress()`, `setResponseBody()`, `setNetworkError()` or `setRequestTimeout()`.

Expand All @@ -140,8 +220,8 @@ Fires a response progress event. Will set the request state to `LOADING`.

Must be preceded by `setResponseHeaders()`.

#### setResponseBody([body = null])
Sets the response body. Calls `setResponseHeaders()` if not already called. Will fire the appropriate 'readystatechange', `progress`, `load`, etc. (upload) events. The state of the request will be set to `DONE`.
#### setResponseBody(body = null)
Sets the response body. Calls `setResponseHeaders()` if not already called. Will fire the appropriate `readystatechange`, `progress`, `load`, etc. (upload) events. The state of the request will be set to `DONE`.

No other mock response methods may be called after this one until `open()` is called.

Expand All @@ -155,11 +235,54 @@ Simulates a request time out. Will set the request state to `DONE` and fire a `t

No other mock response methods may be called after this one until `open()` is called.

### Hooks
The hooks defined in this library can be set at these locations:
- On an instance of `MockXhr` (i.e. of the `XMLHttpRequest` mock class).
- On a "local" `MockXhr` mock subclass returned by `require('mock-xmlhttprequest').newMockXhr()`.
- Globally, directly on the `MockXhr` class (from `require('mock-xmlhttprequest').MockXhr`). Note that each call to `require('mock-xmlhttprequest')` in a node process will return the same instance of `MockXMLHttpRequest`. This means that hooks set directly on `MockXMLHttpRequest.MockXhr` need to be removed manually when no longer needed. This method is therefore not recommended.

#### MockXhr.onCreate(xhr)
Called when an instance of `MockXhr` is created. This makes it possible to capture instances of `XMLHttpRequest` when they are constructed.

This hook is called inside the `MockXhr` constructor.

```javascript
const MockXMLHttpRequest = require('mock-xmlhttprequest');
const MockXhr = MockXMLHttpRequest.newMockXhr();

// Hook for all requests using the local mock subclass
MockXhr.onCreate = (xhr) => { /*...*/ };

// Global hook for all requests from all mocks
MockXMLHttpRequest.MockXhr.onCreate = (xhr) => { /*...*/ };
```

#### MockXhr.onSend(xhr)
Called when `XMLHttpRequest.send()` has done its processing and the test case should start using the mock reponse methods. In a real `XMLHttpRequest`, this would be where the actual http request takes place.

This callback is invoked in an empty call stack (using `setTimeout()`). Therefore you will probably need to use your test framework's asynchronous test support (e.g. for Mocha: https://mochajs.org/#asynchronous-code) to complete the unit test when using this.

```javascript
const MockXMLHttpRequest = require('mock-xmlhttprequest');
const MockXhr = MockXMLHttpRequest.newMockXhr();

// Hook for all requests using the local mock subclass
MockXhr.onSend = (xhr) => { /*...*/ };

// Global hook for all requests from all mocks
MockXMLHttpRequest.MockXhr.onSend = (xhr) => { /*...*/ };

// Hook local to an instance of MockXhr
const xhr = new MockXhr();
xhr.onSend = (xhr) => { /*...*/ };
```

### Run Unit Tests

$ npm test

## Contributing
Contributors are welcome! See [here](CONTRIBUTING.md) for more info.

## License

[ISC](LICENSE)
Loading

0 comments on commit 2209091

Please sign in to comment.