Giter Club home page Giter Club logo

yesno's Introduction

YesNo — Formidable, We build the modern web

Build Status codecov Maintenance Status

YesNo is an HTTP testing library for NodeJS that uses Mitm to intercept outgoing HTTP requests. YesNo provides a simple API to access, manipulate and record requests, using mocks or live services, so that you can easily define what requests should and should not be made by your app.

Note: YesNo is still in beta! We're actively working toward our first major release, meaning the API is subject to change. Any and all feedback is appreciated.

Why?

NodeJS applications often need to generate HTTP requests, whether that is to orchestrate across internal microservices, integrate with third party APIs or whatever. Because the correct behavior of the app is usually dependent on sending the correct request and receiving the correct response, it's important that our tests properly validate our HTTP requests. We can accomplish this through a mix of spying and mocking.

Whereas a naive approach would be to mock the method calls to our request library or configure our application to make requests to a test server, YesNo uses Mitm to intercept HTTP requests at the lowest level possible in process. This allows us to access the request that is actually generated by the application and return an actual response, meaning we can test the actual HTTP behavior of our application, not the behavior of our mocks.

YesNo's sole purpose is to provide an easy interface to intercepting requests and defining mocks. You are free to use your existing assertion library to validate requests.

Installation

npm i --save-dev yesno-http

Usage

To see our preferred usage, skip to recording!

Intercepting live requests

To begin intercepting requests all we need to do is to call yesno.spy(). Afterwards we can access any finished requests we've intercepted by calling yesno.intercepted(). The requests still sent unmodified to its destination, and the client still receives the unmodified response - we just maintain a serialized reference.

const { yesno } = require('yesno-http');
const { expect } = require('chai');
const myApi = require('../src/my-api');

describe('my-api', () => {
 it('should get users', async () => {
   yesno.spy(); // Intercept requests
   const users = await myApi.getUsers();
   const intercepted = yesno.intercepted(); // Get the intercepted requests

   // Intercepted requests have a standardized format
   expect(intercepted).have.lengthOf(1);
   expect(intercepted[0]).have.nested.property('url', 'https://api.example.com/users');
   expect(users).to.eql(intercepted[0].response.body.users); // JSON bodies are parsed to objects
 })
});

Here we assert that only 1 HTTP request was generated by myApi.getUsers() , that the request was for the correct URL and that the return value is equal to the users property of the JSON response body. YesNo will automatically parse the body of JSON requests/responses into an object - otherwise the body will be a string (see ISerializedHttp for the serialized request format).

Mocking responses

A lot of the time when unit testings we don't want our app to hit any external services, but we still want to validate its HTTP behavior. In this case we can call yesno.mock(), which will intercept generated HTTP requests and respond with a provided mock response.

yesno.mock([{
 request: {
   method: 'POST',
   path: '/users',
   host: 'example.com',
   protocol: 'https'
 },
 response: {
   headers: {
     'x-test-header': 'fizbaz'
   },
   body: {
     users: [{ username: 'foobar' }]
   },
   statusCode: 200
 }
}]);

const users = await myApi.getUsers();

expect(users).to.eql(yesno.mocks()[0].response.body.users);

YesNo first checks to make sure the request generated by myApi.getUser() has the same URL as our mock, then responds with the body, status code and headers in our response.

Mocks also allow us to easily test the behavior of our application when it receives "unexpected" responses, such as non-200 HTTP status codes or error response bodies.

Recording Requests

While mocking is useful mocks themselves are hard to maintain. When APIs changes (sometimes unexpectedly!) our mocks become stale, meaning we're testing for the wrong behavior. To solve this problem YesNo allows you to record requests, saving the requests we've intercepted to a local file.

const recording = await yesno.recording({ filename: './get-users-yesno.json' });
await myApi.getUsers();
  expect(yesno.matching(/users/).response()).to.have.property('statusCode', 200);

recording.complete();

This workflow has the advantage of ensuring that our mocks closely represent the real HTTP request/responses our application deals with and making it easy to refresh these mocks when an API has been updated.

To make this workflow even easier, YesNo includes a test method which accepts a jest or mocha style test statement and surrounds it with our record statements. Using the above as an example, we could rewrite it as:

const itRecorded = yesno.test({ it, dir: `${__dirname}/mocks` })

// Mocks for this test will be saved to or loaded from
// "./mocks/get-users-yesno.json"
itRecorded('Get Users', async () => {
  await myApi.getUsers();
  expect(yesno.matching(/users/).response()).to.have.property('statusCode', 200);
})

Now we skip the recording boilerplate and just write our test!

In case you need to load and generate fixtures manually, YesNo also exposes the save and load methods that record uses internally.

Filtering results

Once requests have finished we still need to assert that the requests were correct. We've already seen yesno.intercepted(), which returns all the intercepted requests, but this is just shorthand for yesno.matching().intercepted(), which we can use to selectively access requests.

Consider the following, where we use yesno.matching() to access only the intercepted user request, then assert a password was hashed.

yesno.spy();

await myApi.complicatedAuthFlow(token); // Lots of HTTP requests!
await myApi.updateUser(userId, rawPassword);

expect(
 // Match only requests with this url
 yesno.matching(`https://example.com/users/${userId}`).intercepted()[0]
).to.have.nested.property("request.body.password", hash(rawPassword));

We can even use this syntax to selectively redact values from the serialized requests, so that we don't persist sensitive data to our mocks. This is a common problem when auth tokens are being sent back and forth between the APIs.

await myApi.complicatedAuthFlow(token); // Lots of HTTP requests!
await myApi.updateUser(userId, rawPassword);

yesno.matching(/auth/).redact(['request.headers.authorization', 'response.body.token']);


expect(yesno.matching(/auth/).intercepted()).to.have.nested.property(
 'request.headers.authorization', '*****');

await yesno.save(testName, dir); // Recorded mocks are sanitized

The matching method can filter on any of the properties in the serialized object. See the API documentation for more examples.

Mocking matched results

Matched results can be replaced with static or dynamic mocked responses. Use the .respond() method on a filtered http collection to define the response.

yesno.matching(/get/).respond({ statusCode: 400 })

yesno.matching({ request: { path: '/post' } }) .respond((request) => ({ statusCode: 401, body: request.body }))

Responses defined in this way take precedence over normally loaded mocks.

Ignoring matched mocks to proxy requests

Matched requests can ignore the defined mocks and proxy the request to the original host. This provides mixing of live and mocked results. Use the .ignore() method on a filtered http collection to disable the mock.

Matching requests set in this way take precedence over all defined and loaded mocks.

Restoring HTTP behavior

When we no longer need YesNo to intercept requests we can call yesno.restore(). This will completely restore HTTP behavior & clear our mocks. It's advisable to run this after every test.

describe('api', () => {
 beforeEach(() => yesno.spy()); // Spy on each test
 afterEach(() => yesno.restore()); // Cleanup!

 describe('lots of tests with lots of requests', () => { ... });
});

If you're using yesno.test() it'll call restore for you whenever it runs.

Examples

Visit the examples directory to see sample tests written with YesNo.

You can run the tests yourselves after cloning the repo.

npm install
npm run example-server # Start test server

Then in a separate window

npm run example-tests

API

YesNo is written in TypeScript and uses its type syntax where possible.

To see typedoc generated documentation, click here.

YesNo

The yesno instance implements all the methods of the FilteredHttpCollection interface.

yesno.spy(options?: IInterceptOptions): void

Enables intercept of requests if not already enabled.

IInterceptOptions

options.ignorePorts: number[]: Important. Since YesNo uses Mitm internally, by default it will intercept any sockets, HTTP or otherwise. If you need to ignore a port (eg for a database connection), provide that port number here. Normally you will run YesNo after long running connections have been established, so this won't be a problem.

yesno.mock(mocks: ISerializedHttp[] | ISerializedHttpMock[], options?: IInterceptOptions): void

Enables intercept of requests if not already enabled and configures YesNo to respond to all forthcoming intercepted requests with the provided mocks.

YesNo responds to the Nth intercepted request with the Nth mock. If the HTTP method & URL of the intercepted request does not match the corresponding mock then the client request will fail.

When YesNo cannot provide a mock for an intercept it emits an error event on the corresponding ClientRequest instance. Most libraries will handle this by throwing an error.

See also IInterceptOptions.

yesno.recording(options?: IInterceptOptions & IFileOptions): Promise<Recording>

Begin a new recording. Recording allow you to alternatively spy, record or mock behavior according to the value of the environment variable YESNO_RECORDING_MODE. The values and the accompanying behaviors of theses modes are described below.

Mode Value Description
Spy "spy" Intercept requests & proxy to destination. Don't save. Equivalent to yesno.spy()
Record "record" Intercept requests & proxy to destination. Save to disk on completion. Equivalent to yesno.spy() & yesno.save()
Mock "mock" (default) Load mocks from disks. Intercept requests & respond with mocks. Don't save. Equivalent to yesno.mock(await yesno.load()).

Example

// Begin a recording. Load mocks if in "mock" mode, otherwise spy.
const recording = await yesno.recording({
  filename: './get-users.json'
})

// Make our HTTP requests
await myApi.getUsers()

// Run assertions
expect(yesno.matching(/users/).response()).to.have.property('statusCode', 200)

// Persist intercepted requests if in "record" mode, otherwise no-op
await recording.complete()
yesno.test(options: IRecordableTest): (name: string, test: () => Promise<any>) => void

A utility method for creating test definitions instrumented with yesno.recording(). It accepts any testing method it or test which accepts a name and test function as its arguments, along with a directory and optional prefix to use for recording fixtures.

IRecordableTest

options.test: (name: string, test: () => Promise<any>) => any: A test function, such as jest.test or mocha.it which accepts a name and test definition. The test may either be synchronous or return a promise.

options.it: (name: string, test: () => Promise<any>) => any: Alias for options.test

options.dir: string: Directory to use for recording

options.prefix?: string: Optional. Prefix to use for all fixtures. Useful to prevent conflicts with similarly named tests in other files.

Example

Given the below test written with yesno.recording....

it('should get users', async () => {
  const recording = await yesno.recording({ filename: `${__dirname}/mocks/should-get-users-yesno.json` });
  await myApi.getUsers();
  await recording.save()
})

...we may write it more concisely with yesno.test as

const itRecorded = yesno.test({ it, dir: `${__dirname}/mocks` });

itRecorded('should get users', async () => {
  await myApi.getUsers();
});

Which removes much of the boilerplate from our test.

yesno.restore(): void

Restore normal HTTP functionality by disabling Mitm & restoring any defined stubs. Clears references to any stateful properties such as the defined mocks or intercepted requests.

If you're using YesNo in a test suite it's advisable to run this method after every test case.

yesno.save(options: IFileOptions & ISaveOptions): Promise<void>

Save serialized HTTP requests to disk. Unless records are provided directly, yesno will save the currently intercepted requests.

You may provide a filename in the options object or use the name & directory shorthand to generate a filename from a human readable string.

const testName = 'should hit the api'
yesno.save(testName, mocksDir) // => "./test/mocks/should-hit-the-api-yesno.json"

Unless providing records, this method will throw an error if there are any in flight requests to prevent users from accidentally saving before all requests have completed.

IFileOptions

options.filename: string: Full filename (JSON)

ISaveOptions

options.records?: ISerializedHttp[]: Records to save. Defaults to already intercepted requests.

yesno.load(options: IFileOptions): Promise<ISerializedHttp[]>

Load serialized HTTP requests from a local JSON file.

See IFileOptions.

yesno.matching(filter?: HttpFilter): FilteredHttpCollection

Apply a filter to subsequently access or manipulate matching mocks or intercepted requests.

We define an HttpFilter as: type HttpFilter = string | RegExp | ISerializedHttpPartialDeepMatch | (serialized: ISerializedHttp) => boolean;

The filter is applied to each serialized request to filter results. If the filter is...

  • A string: Perform an exact match on URL (port optional)
  • A regular expression: Test against URL (port optional)
  • An object (ISerializedHttpPartialDeepMatch): Perform a deep partial comparison against the serialized request
  • A function: A callback that receives the ISerializedHttp object and returns a boolean value of true to indicate match.
  • undefined: The entire collection is returned.

Examples:

yesno.matching('https://api.example.com/users'); // Exact match on url
yesno.matching(/example/); // Any request to Example website
yesno.matching({ // Any POST requests to Example with status code of 500
 request: { host: 'example.com', method: 'POST' },
 response: { statusCode: 500 }
});
yesno.matching((serialized, i) => {
 if (i === 0) { // First request
   return true;
 }

 const { request } = serialized;
 if (request.body.firstName === request.body.lastName) { // Custom logic
   return true;
 }

 return false;
});
yesno.matching().response(); // short-cut to get the response from the one intercepted request

FilteredHttpCollection

collection.mocks(): ISerializedHttp[]

Return the mocks defined within the collection.

collection.ignore(): ISerializedHttp

Ignore any mocked responses for all matching requests. Matching requests will be proxied to the host.

Any matching requested set in this way take precedence over all defined and loaded mocks.

collection.intercepted(): ISerializedHttp[]

Return the intercepted requests defined within the collection.

collection.redact(property: string | string[], redactor: Redactor = () => "*****"): void

property: Property or array of properties on serialized requests to redact. redactor: Callback that receives value and property path on matching requests. Return value will be used as redacted value.

Redact properties on intercepted requests within the collection. Nested properties may be indicated using ..

Example

await myApi.getToken(apiKey)

// Replace the auth values with an md5 hash
yesno.matching(/login/).redact(
  ['request.headers.authorization', 'response.body.token'],
  (value, path) => md5(value)
)
await yesno.save({ filename: 'redacted.json' })
collection.request(): ISerializedHttp

Return the request part of the single matching HTTP request.

Throws an error if the collection does not match one and only one request.

Example

await myApi.getUsers(token);

expect(yesno.matching(/users/).request()).to.have.nested.property('headers.authorization', token)
collection.respond(): ISerializedHttp

Provide a mock response for all matching requests. Optionally provide a callback to dynamically generate a response for each request.

Any matching responses defined in this way take precedence over normally loaded mocks.

collection.response(): ISerializedHttp

Response corollary to collection.request(). Return the response part of the single matching HTTP request.

Throws an error if the collection does not match one and only one request.

Recording

recording.complete(): Promise<void>

Save the request if we're in record mode. Otherwise no-op.

ISerializedHttp

interface ISerializedHttp {
 readonly __id: string;
 readonly __version: string;
 readonly __timestamp: number;
 readonly __duration: number;
 readonly request: SerializedRequest;
 readonly response: SerializedResponse;
}

export interface SerializedResponse {
 readonly body: string | object;
 readonly headers: IncomingHttpHeaders;
 readonly statusCode: number;
}

export interface SerializedRequest {
 readonly body?: string | object;
 readonly headers: OutgoingHttpHeaders;
 readonly host: string;
 readonly path: string;
 readonly method: string;
 readonly port: number;
 readonly query?: string;
 readonly protocol: 'http' | 'https';
}

Maintenance Status

Archived: This project is no longer maintained by Formidable. We are no longer responding to issues or pull requests unless they relate to security concerns. We encourage interested developers to fork this project and make it their own!

yesno's People

Contributors

boygirl avatar carbonrobot avatar chadkirby avatar cpresler avatar dependabot[bot] avatar estrada9166 avatar ianwsperber avatar keithcom avatar kschat avatar mscottx88 avatar ryan-roemer avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

yesno's Issues

Discovery: Automatically bypass sockets for non-HTTP

Mitm will intercept any socket. Currently we require the user to provide ports they would like to ignore. It'd be nice if YesNo could implement some heuristic to detect sockets for HTTP requests and only intercept those. Hard because I'm not sure whether we can guarantee that the options object passed to Mitm's .on("connect" callback will have certain properties on all HTTP requests. If it's possible that'd be a good way of doing this.

Recording fails

Trying to follow example fails:

        const recording: object = await yesno.recording({
          filename: './test-recording-yesno.json',
        });
Original error: ENOENT: no such file or directory, open 'E:\mscottx88\oss-project\coc-poc\test-recording-yesno.json'
      at ReadFileContext.fs_1.readFile [as callback] (node_modules\yesno-http\dist\src\file.js:25:35)
      at FSReqWrap.readFileAfterOpen [as oncomplete] (fs.js:420:13)

Allow forcing save

From the docs:

Unless providing records, this method will throw an error if there are any in flight requests to prevent users from accidentally saving before all requests have completed.

We should allow users to override this check. Some fools may want to save requests anyways :P

Questions on detailed behavior of matching().respond()

It's not clear from the docs or tests some cases for using matching().respond().

  1. What happens if I setup
matching(/A).respond()
matching(/B).respond()

then I call these two paths out of order B, A?

  1. What happens if I have the same setup and call A, B, B?

If the docs and tests don't cover these questions can they be added?

Test: Enable coverage, pass it, and badge it.

  • Make sure we pass our current coverage requirements
  • Hook up Codecov (seems to work better than coveralls for TS, but any service really is fine)
  • Add a badge to show off our fine coverage percentage.

Redacting headers should be case insensitive

Both yesno.redact('request.headers.Foobar') and yesno.redact('request.headers.foobar') should be valid. Currently only the lower case version applies.

It'd also be nice if there was some validation on the property names...

Support Node 11.1 & 11.2

There were some recent changes in the Node internals that are causing Mitm to break. It appears this it actively being worked on moll/node-mitm#55. We'll want to reference the new Mitm version & add Node 11 back to CI once it's in.

Support non-deterministic order of HTTP requests.

Currently the order of the mocks must match the order of the HTTP requests. However some applications will have a non-deterministic order of HTTP requests. We could support these apps by:

  1. Support providing a callback to match an HTTP request to a mock
  2. Support matching HTTP to a mock by URL, ignoring order (could be configurable).

Would be great to get feedback on usage before making a decision.

Comparison to nock

Many features of this library achieve the same result as the popular https://github.com/nock/nock package. Should we provide a comparison chart of the differences and pros/cons to help developers choose the correct library for their use case?

Feature: Implement yesno.mockRule().live()

Add a live method to the yesno Rules class. This method will set the action for the last rule added to the internal array to live. A live action will proxy the request and keep the response live.

Feature: Implement yesno.mockRule().record

Add a record method to the yesno Rules class. This method will set the action for the last rule added to the internal array to record. A record action will either record or mock the request depending on the set env mode.

Integration tests failing in node v12

set node version to v12.18 (also fails in v12.3 and probably all of v12)
run 'yarn tests'
see integration test errors

  yesno
    ✓ should send get to test server
    ✓ should proxy HTTP GET requests (485ms)
    1) should proxy HTTP POST requests
    2) should mock HTTPS requests
    #save
      - should create records locally
    #intercepted
      ✓ should allow querying for the various requests made
      ✓ should treat JSON request or response bodies as objects
    #redact
      ✓ should allow redacting a single nested property
    mock mode
      ✓ should play back the requests from disk


  6 passing (11s)
  1 pending
  2 failing

  1) yesno
       should proxy HTTP POST requests:
     Error: Timeout of 5000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/keith/Formidable/yesno/test/integration/yesno.spec.ts)
      at listOnTimeout (internal/timers.js:531:17)
      at processTimers (internal/timers.js:475:7)

  2) yesno
       should mock HTTPS requests:
     Error: Timeout of 5000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/Users/keith/Formidable/yesno/test/integration/yesno.spec.ts)
      at listOnTimeout (internal/timers.js:531:17)
      at processTimers (internal/timers.js:475:7)



npm ERR! code ELIFECYCLE
npm ERR! errno 2
npm ERR! [email protected] integration: `mocha --timeout 5000 "test/integration/**/*.spec.ts"`
npm ERR! Exit status 2
npm ERR! 
npm ERR! Failed at the [email protected] integration script.
npm ERR! This is probably not a problem with npm. There is likely additional logging output above.

npm ERR! A complete log of this run can be found in:
npm ERR!     /Users/keith/.npm/_logs/2020-06-12T16_38_16_098Z-debug.log
error Command failed with exit code 2.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Feature: Implement yesno.mockRule().respond()

Add a respond method to the yesno Rules class. This method will set the action for the last rule added to the internal array to respond and save the passed response. A respond action will mock the request with the provided response.

First class support for browser testing

Use cases for browser testing not well supported today:

  • Requests arrive in arbitrary order (mentioned as "smart" mode in #64)
  • Requests may be mid flight when we end test

Feature: Implement yesno.mockRule().redact()

Add a redact method to the yesno Rules class. This method will set the action for the last rule added to the internal array to redact and save the list of properties to redact. A redact action will update properties in the intercepted array to redact their values to prevent sensitive information from being saved to disk.

Allow intercepting only specified requests

As a developer, I would like to be able to intercept only specified network requests.

Today YesNo takes an "all or nothing" approach to HTTP interception. Such an approach works well in a context where all outbound HTTP requests are deterministic and bounded. However if the numbers of requests is unknown or the requests may occur in an unknown order, this approach breaks down when we attempt to use mocks. Consider the below example:

const itRecorded = yesno.test({ it, dir: `${__dirname}/mocks` });

itRecorded('should make at least 1 request to Formidable', () => {
  await myApi.getUsers();

  expect(yesno.matching(/formidable/).intercepted().length). toBeGreaterThanOrEqual(1);
});

In this test, all we care is that at least 1 request was made to formidable. This works fine when spying on requests. However when running against mocks, the test will fail if the requests made by getUsers do not not match the mocks, which could happen either because:

  1. The number of requests made exceed the number of mocks
  2. The requests made occur in a different order than the mocks

There are 2 solutions I'm considering for this use case, the first of which is the subject of this issue:

  1. Allow specifying what specific requests you wish to intercept using matching().
  2. Add some sort of "smart" mode to mocking, where YesNo will attempt to find a best match from your mocks, regardless of order.

For the first, I'd propose adding roughly the following functionality to the YesNo API:

it('should make at least 1 request to Formidable', () => {
  // Order determines precedence; requests to formidable match first, everything else matches second
  yesno.matching(/formidable/).mock({ statusCode: 200, body: { foo: 'bar' }  });
  // Allow providing a function to drive the response off the request
  yesno.matching().mock((request) => 
    request.header['x-fizbaz'] ? { statusCode: 500 } : { statusCode: 200, body: { fiz: 'baz' } }); 

  await myApi.getUsers();

  expect(yesno.matching(/formidable/).intercepted().length). toBeGreaterThanOrEqual(1);
});

Note that this essentially adds the core functionality of nock, which is to provide a mock response for a specified path. To further allow providing a custom response, it may make sense to

I believe this should greatly increase the usability of the library for those new to this testing approach, and will make it easier to recommend this library over nock. I believe @samwhale encountered a use case for this recently and may be able to weigh on whether it'd address his need.

Finalize CLI

We've including a CLI tool to help users generate mocks & potentially do other work. Haven't really put any thought into whether there are other commands that'd be useful. Also need to document for the users.

  • Finalize CLI commands
  • Document

If missing mock file error message should prompt user to record

We currently get the following error message if a mock file is missing:

Error: YesNo: Mock file for "should-log-in-yesno" does not exist. To generate the missing file now you may run the command:

./node_modules/.bin/yesno generate "/Users/typer/repos/minotaur/test/spec/functional/graphql/mocks/should-log-in-yesno.json"

However if the user is using the "itRecorded" functionality they probably just want to run the test in record mode. Accordingly, error message should prompt the user to change the mode.

Allow setting comparator for matching mocks

collection.comparator((intercepted: ISerializedRequest, mock: ISerializedRequest) => boolean): void

Provide a custom comparator to use with mocks within the collection. The comparator is used to determine whether an intercepted request matches a mock. Use this method to make mocking more or less strict, which can reduce/increase the need to validate intercepted requests afterward. YesNo ships with the comparators comparators.url, comparators.body, comparators.headers. By default YesNo will use comparators.url (least strict).

You can compose comparators to mix and match behavior:

const { comparators } = require('yesno-http');
const { flow } = require('lodash'); // Composition helper

const compareAuthHeader = (intercepted, mock) => intercepted.headers.authorization === mock.headers.authorization;
yesno.matching(/auth/).comparator(flow(comparators.url, compareAuthHeader));

Allow accessing `response()` and `request()` without providing a matcher

Users should be able to access response() and request() methods for getting a single intercepted request/response without providing a matcher. We could either A: Making the matcher filter optional B: Make request/response available on yesno directly.

I prefer option A, because we will only bloat the API if we continue to add methods from matching() onto yesno as shortcuts.

For an example, see https://github.com/estrada9166/yesno-easygraphql/blob/master/test/easygraphql-tester.js#L46:

const response = yesno.matching(/\//).response()

which we would like to change to:

const response = yesno.matching().response()

Tests fail for Node 10

Tests are failing in CI for Node 10. This is occurring for commits that previously succeeded, so is likely due to a change either in Node or the request libraries.

Example:

https://travis-ci.com/FormidableLabs/yesno/jobs/183919449

  18) Yesno
       #mock
         should accept an optional comparatorFn, which can accept a mock when it does not throw:
     RequestError: TypeError: Cannot set property 'clientRequest' of undefined
      at new RequestError (node_modules/request-promise-core/lib/errors.js:14:15)
      at Request.plumbing.callback (node_modules/request-promise-core/lib/plumbing.js:87:29)
      at Request.RP$callback [as _callback] (node_modules/request-promise-core/lib/plumbing.js:46:31)
      at self.callback (node_modules/request/request.js:185:22)
      at Request.start (node_modules/request/request.js:753:10)
      at Request.end (node_modules/request/request.js:1511:10)
      at end (node_modules/request/request.js:564:14)
      at Immediate._onImmediate (node_modules/request/request.js:578:7)

Apply redact to incoming requests

Currently the redact() method will redact properties on the intercepted requests, so that the mocks we save to disk do not contain sensitive information. However when we subsequently run tests with these mocks the generated requests will still have their original values, meaning there could be a mismatch between the mocks and the generated requests. We could fix this by having the redact method run against all requests intercepted after it is run. So if you didn't want to redact properties from forthcoming intercepted requests, only for the generated mocks, then you'd just run redact after your requests. Otherwise you would run it before.

I think this needs to be sorted out before the v1.0 release. This has caused some confusing behavior when I've used libs like nocktor in the past.

Should mocks defined using `matching().respond()` clobber mocks from `yesno.mock`

See

it('should support mock override with respond', async () => {

If we defined mocks for calls to /A, /B, /C, yesno will try to match those 3 mocks in order of API calls made. If the 2nd API call, for example, isn't to /B it will error.

A user then adds matching('/X').respond() in addition to the mocks for A, B, C above. If the user then has API calls in this order:
A, B, X, C

They will encounter an error on the call to C b/c in fact X has replaced C so C mock will no longer exist.

In fact to get this to work I would have to:

  • create mocks ``/A, /B`, `/DUMMY_TO_GET_CLOBBERED`, /C`.
  • create matching('/X').respond()
  • ✅ call A, B, X, C

No proposed updated API here. For now I just wanted to note this behavior and consider if it's what we want.

Feature: Implement yesno.mockRule

Implement the new yesno.mockRule feature as defined in docs/DESIGN.md. It should be passed an HttpFilter (like the matching method) and append it to the internal array of rules. On requests, the rules should be evaluated and applied before any actions.

Feature: Implement yesno.mockMatchingFunction()

Add a mockMatchingFunction method to the yesno class. This method will be passed a callback function that will be used to override the default internal matching function for matching requests with rules or the recorded mock array. The callback will be called with an object:

 {
  currentRequest,   // object with the request being processed
  recordedMocks,   // array of recorded mocks
  mockRules,          // array of rules
  maxMockCount   // the maximum number of times a mock can be used (defaults to 1, 0 is unlimited)
}

Feature: Implement yesno.mockMode

By default, each recorded mock will be matched and used one time. This is considered strict mode, but the user could change this to a firstMatch mode where the first matching mock is used and the same mock could be used multiple times.

Support Node 6

  • Replace async/await in API with Promises
  • Update package.json:engines restriction which currently restricts to node8+

Export Recording type for use in TypeScript apps

When trying to follow the example

const recording = await yesno.record({ filename: './get-users-yesno.json' });
await myApi.getUsers();
  expect(yesno.matching(/users/).response()).to.have.property('statusCode', 200);

recording.complete();

There is no access to the data type Recording for declaration on the recording variable.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.