Giter Site home page Giter Site logo

unmock-js's Introduction

Unmock (JavaScript SDK)

CircleCI codecov Known Vulnerabilities Chat on Gitter

Fuzz test your REST API calls.

Install

$ npm install --save-dev unmock

What Does Unmock Do

Unmock helps you fuzz test REST API calls. Fuzz testing, or fuzzing, is a form of testing where you verify the correctness of code by asserting that it behaves correctly with variable and unexpected input. The response of a REST API is most often variable and unexpected. So, in most cases, fuzz testing is a useful way to test integrations with APIs.

When To Use Unmock

Below are some questions to ask when determining if Unmock is a good fit for your development process.

  • Is my code base in JavaScript or TypeScript?
  • Does my code base have tests written in Jest, Mocha, Jasmine, Tap or Ava?
  • Do I make a network call from my codebase to a REST API?

If the answer is yes to all of these questions, Unmock could help you test code paths in your code that make REST API calls and use the responses from those API.

The Docs

If you don't like reading READMEs, check out our docs!

Usage

Here is a mock defined using unmock, a function to be tested, and a test written in jest. The commented numbers are explained in the text below this code example.

const fetch = require("node-fetch");
const unmock = require("unmock");
const { runner, transform, u } = unmock;
const { withCodes } = transform;

unmock
  .mock("https://zodiac.com", "zodiac")
  .get("/horoscope/{sign}") // 1
  .reply(200, {
    horoscope: u.string(), // 2
    ascendant: u.opt(u.string()) // 3
  })
  .reply(404, { message: "Not authorized" }); // 4

async function getHoroscope(sign) {
  // use unmock.fetch, request, fetch, axios or any similar library
  const result = await fetch("https://zodiac.com/horoscope/" + sign);
  const json = await result.json();
  return { ...json, seen: false };
}

let zodiac;
beforeAll(() => {
  zodiac = unmock.default.on().services.zodiac;
});
afterAll(() => unmock.default.off());

describe("getHoroscope", () => {
  it(
    "augments the API call with seen=false",
    runner(async () => { // 5
      zodiac.state(withCodes(200)); // 6
      const res = await getHoroscope();
      expect(res).toMatchObject(JSON.parse(zodiac.spy.getResponseBody())); // 7
      expect(res.seen).toBe(false);
    }),
  );
});

With unmock, you can (1) override a REST endpoint to provide (2) variable and (3) optional responses in addition to (4) different status codes. Then, one uses the (5) runner to automatically run a test multiple times with subtly different responses from the API. One can also (6) initialize the API to a given state and (7) make assertions about how an API was used.

Specifying hostname

The request hostname should be a string.

unmock.mock('http://www.example.com')
  .get('/resource')
  .reply(200, u.integer())

Unmock will then refer to the service as example. To specify the name of the service as foo, we would write.

unmock.mock('http://www.example.com', 'foo')
  .get('/resource')
  .reply(200, u.string())

To match multiple protocols or URLs, use associate.

unmock.default.associate('https://www2.example.com', 'foo')

Specifying the path

The request path should be a string, and you can use any HTTP verb. Wild-cards in the string should be enclosed in curly braces.

unmock.mock('http://www.example.com')
  .get('/resource/{id}')
  .reply(200, u.string())

Alternatively, you can use an array of path segments, where each segment is either a string, a string in curly-braces for an open parameter, or a key/value pair associating a name of a path parameter to a regex matching it.

unmock.mock('http://www.example.com')
  .get(["users", /[0-9]+/, "status"]) // "/users/{id}/status"
  .reply(200, u.string())

Specifying request body

You can specify the request body to be matched as the second argument to the get, post, put or delete specifications. The argument can be any valid JSON, json-schema-poet using the u syntax(u.integer(), u.string()) or any combination thereof.

Here is an example of a post request that will only validate if it contains a token.

unmock.mock('http://www.example.com')
  .post('/login', u.type({ token: u.string()}, {expires: u.integer()}))
  .reply(200, { id: u.string() })

Unmock does not support body encodings other than "application/json" at this point.

Specifying request query string

Unmock automatically ignores query strings. However, it understands query strings if you would like to match against them.

When query strings are included in Unmock, they will act as if they are required. Most APIs do not have required query strings, so make sure to double-check the API documentation before indicating a required query string.

These parameters can be included as part of the path:

unmock.mock('http://example.com')
  .get('/users?foo=bar')
  .reply(200)

Instead of placing the entire URL, you can specify the query part as an object:

unmock.mock('http://example.com')
  .get('/users')
  .query({ name:u.string(), surname: u.string() })
  .reply(200, { results: u.array({id: u.integer() }) })

Specifying Request Headers

You can specify the request headers like this:

unmock.mock('http://www.example.com', {
  reqheaders: {
    authorization: 'Basic Auth',
  },
})
  .get('/')
  .reply(200)

Or you can use a regular expression to check the header values.

unmock.mock('http://www.example.com', {
  reqheaders: {
    'X-My-Awesome-Header': /Awesome/i,
  },
})
  .get('/')
  .reply(200)

Headers in unmock are always a partial match, meaning that additional headers are ignored. This means that you don't need to worry about matching against common headers like Content-Type and Host.

Specifying replies

You can specify the return status code for a path on the first argument of reply like this:

unmock.mock('http://myapp.iriscouch.com')
  .get('/users/1')
  .reply(404)

You can also specify the reply body as valid JSON, json-schema-poet, or any combination thereof.

unmock.mock('http://www.google.com')
  .get('/')
  .reply(200, u.stringEnum(['Hello from Google!', 'Do no evil']))

If you would like to transform any part of a constant reply (ie a fixture recorded from real API traffic) into a variable version of itself, use u.fuzz. This command infers the type of its input and produces output following the same schema.

unmock.mock('http://www.foo.com')
  .get('/')
  // produces { firstName: "a random string", lastName: "another random string" }
  .reply(200, u.fuzz({ firstName: "Bob", lastName: "Smith" }))

Specifying reply headers

You can specify the reply headers like this:

unmock.mock('https://api.github.com')
  .get('/repos/atom/atom/license')
  .reply(200, { license: 'MIT' }, { 'X-RateLimit-Remaining': u.integer() })

Chaining

You can chain behavior like this:

unmock.mock('http://myapp.iriscouch.com')
  .get('/users/1')
  .reply(404)
  .post('/users', {
    username: u.string(),
    email: u.string(),
  })
  .reply(201, {
    ok: u.boolean(),
    id: u.string(),
    rev: u.string(),
  })
  .get('/users/123ABC')
  .reply(200, {
    _id: u.string(),
    _rev: u.string(),
    user: u.string(),
    name: u.string()
  })

Ignorable API calls

For ignorable API calls where you are passing through information but don't care about a response, you can instruct unmock to serve random 200 responses to those requests using tldr.

unmock
   .mock("https://my-analytics-api.vendor.com)
  .tldr();

Expectations

Unmock uses sinon spies to help you compose (great) expectations.

In general, try to write expectations using spies instead of hardcoded values. Instead of expect(res).toEqual({foo: "bar", baz: true }), favor expect(res).toEqual({ ...spy.getRequestBody(), baz: true })

Assuming you have defined a service using unmock, you can use spies following the <verb><Request|Response><Thing> convention. For example, getResponseBody or deleteRequestPath. Nonsensical things like getRequsetBody are not defined.

test("analytics API was called", async () => {
  await myFunction()
  expect(analyticsService.spy.postRequestBody())
    .toMatchObject({ event: "VISITED" })
})

The following getter functions are defined on the spy object.

  • getRequestPathname
  • getRequestPath
  • getRequestHeaders
  • getRequestQuery
  • getRequestProtocol
  • getResponseBody
  • getResponseCode
  • getResponseHeaders
  • postRequestBody
  • postRequestPathname
  • postRequestPath
  • postRequestHeaders
  • postRequestQuery
  • postRequestProtocol
  • postResponseBody
  • postResponseCode
  • postResponseHeaders
  • putRequestBody
  • putRequestPathname
  • putRequestPath
  • putRequestHeaders
  • putRequestQuery
  • putRequestProtocol
  • putResponseBody
  • putResponseCode
  • putResponseHeaders
  • deleteRequestPathname
  • deleteRequestPath
  • deleteRequestHeaders
  • deleteRequestQuery
  • deleteRequestProtocol
  • deleteResponseBody
  • deleteResponseCode
  • deleteResponseHeaders

In addition, spies are full-fledged sinon spies. More about their usage in Unmock can be found here, and more information on sinon can be found here.

Initializing Mocks

Unmock supports the initialization of services to arbitrary states. This is helpful, for example, if you want to test how your code behaves when a given service returns exactly five items or when a particiular field in an object is missing or present.

To do this, set the state property of a service. The state property is a function that takes a request and an OpenAPI schema as input and returns an OpenAPI schema and output. Many utility functions have been created for the most common state manipulations. For example, to test the outcome with only certain codes, use withCodes.

const unmock = require('unmock');
const withCodes = unmock.transform.withCodes;
const github = unmock.default.on().services.github;
github.state(withCodes([200, 201, 404]));

Because withCodes returns a function, the same thing could have been written.

const unmock = require('unmock');
const withCodes = unmock.transform.withCodes;
const github = unmock.default.on().services.github;
github.state((req, schema) => withCodes([200, 201, 404])(req, schema));

This is useful, for example, if you want to test a certain code only if a given header is present.

const unmock = require('unmock');
const withCodes = unmock.transform.withCodes;
const github = unmock.default.on().services.github;
github.state((req, schema) =>
  withCodes([200, ...(req.headers.MyHeader ? [404] : [])])(req, schema));

The unmock documentation contains more information about initializing the state.

Faker API

UnmockFaker class provides a lower-level API for working with mocks. You can create a new UnmockFaker via unmock.faker():

const unmock, { Service, ISerializedRequest} = require("unmock");
// import unmock from "unmock";  // ES6

const faker = unmock.faker();

To use the faker for mocking, you need to add services. The first option is to use the mock method:

faker
  .mock("http://petstore.swagger.io", "petstore")
  .get("/v1/pets")
  .reply(200, { foo: u.string() });

Alternatively, you can create a service from OpenAPI specification with Service.fromOpenAPI:

const { Service } = require("unmock");

const schema: OpenAPIObject = ...; // Load OpenAPIObject
const petstoreService = Service.fromOpenAPI({ schema, name: "petstore" })

// Add service to `faker`
faker.add(petstoreService);

You can then also modify the state of the petstore service via state property:

const { transform } = require("unmock");
// Service should always return code 200
petstore.state(transform.withCodes(200));

Once you have added a service, you can use faker.generate method to create a mock for any Request object:

const { UnmockRequest, UnmockResponse } = require("unmock");

const req: UnmockRequest = {
  host: "petstore.swagger.io",
  protocol: "http",
  method: "get",
  path: "/v1/pets",
  pathname: "/v1/pets",
  headers: {},
  query: {},
}

const res: UnmockResponse = faker.generate(req);

// Access res.statusCode, res.headers, res.body, etc.
expect(res.statusCode).toBe(200);

Runner

With the Unmock runner, you can run any test multiple times with different potential outcomes from the API. All of your unmock tests should use the runner unless you are absolutely certain that the API response will be the same every time.

Default

By default, the runner is set to run your test 20 times. If you want to change this value, please refer to our docs on changing the runner default.

Jest

A Jest configuration for the runner is available through a separate unmock-jest-runner package. While the standard unmock-runner is available via NPM, you'll want to use the unmock-jest-runner when executing your tests to ensure proper error handling.

The unmock-jest-runner can be installed via NPM or Yarn:

npm install -D unmock-jest-runner
yarn add unmock-jest-runner

Once installed, the runner can be imported as a default and used as a wrapper for your tests:

const runner = require("unmock-jest-runner").default;

test("my API always works as expected", runner(async () => {
  const res = await myApiFunction();
  // some expectations
}));

Other configurations

As of now, Jest is the only package we have available.

However, we're currently building out support for Mocha and QUnit. You can follow the progress of those implementations in the corresponding issues.

OpenAPI

Unmock supports the reading of OpenAPI specifications out of the box. Place your specification in a folder at the root of your project called __unmock__/<myspecname>, where <myspecname> is the name of the spec on the unmock.on().services object. Several examples of this exist on the internet, most notably here.

Tutorials

Contributing

Thanks for wanting to contribute! Take a look at our Contributing Guide for notes on our commit message conventions and how to run tests.

Please note that this project is governed by the Meeshkan Community Code of Conduct. By participating in this project, you agree to abide by its terms.

Development

  • See publish.md for instructions how to make a new release

License

MIT

Copyright (c) 2018–2019 Meeshkan and other contributors.

unmock-js's People

Contributors

baeyun avatar carolstran avatar dependabot[bot] avatar idantene avatar k4m4 avatar ksaaskil avatar meeshkancain 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  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

unmock-js's Issues

faker seems to be broken

Examples I get for u.string("name.firstName"):

  • 'tempor exercitation'
  • 'non fugiat Lorem id'
  • 'ex tempor consequat minim'
  • 'Ut ullamco minim magna aliquip'
  • ...

versions:

  "devDependencies": {
    "@types/jest": "^24.0.18",
    "jest": "^24.9.0",
    "prettier": "^1.18.2",
    "ts-jest": "^24.1.0",
    "typescript": "^3.6.3",
    "unmock": "^0.3.4"
  },

Propose and implement a good documentation convention for OpenAPI refinements

OpenAPI refinements is pretty hard to understand. The basic idea is that it transforms OpenAPI schemas into subsets of those schemas. For example, it allows you to remove certain methods (ie remove a POST method from an endpoint), allows you to constrain an array in a response with a minimum or maximum length, etc.

The idea of "subset" is a bit confusing here, at least to me. Subset refers to the outcome space. For example, imagine that an Open API schema says that endpoint GET kittens can return an array of Kitten objects. A valid subset of the outcome space may be all arrays of kittens with at least three values. That is, array with min length three are valid possible outcomes for GET kittens, but are not exhaustive of all possible outcomes, as there may be 0, 1 or 2 kittens in real life.

Narrowing the outcome space is how unmock makes transformations in tests like "serve me an array of at least three kittens when I run this test" or "always 404 when I hit this endpoint in a test".

OpenAPI refinements does this by using monocle-ts. Monocle TS allows you to drill down into arbitrarily complex data structures and get/set little bits of them. In the case of a setter, it returns a copy of the data structure with the "setter" part changed and everything else staying the same.

Code with lenses tends to be super functional-looking, which is a pleasure to write and (IMO) hard to read. For example, take the following code:

const lensToOperations = (
  path: RegExp | boolean,
  operations: MethodNames[] | boolean,
) =>
  lensToPath(path)
    .composeIso(objectToArray<any>())
    .composeTraversal(
      fromTraversable(array)<[string, any]>().filter(i =>
        typeof operations === "boolean"
          ? operations
          : operations.map(z => `${z}`).indexOf(i[0]) !== -1,
      ),
    )
    .composeLens(valueLens())
    .composePrism(
      new Prism<any, Operation>(s => (isOperation(s) ? some(s) : none), a => a),
    );

It is a composition of optic primitives in the functional style, but it is too opaque to be maintainable by anyone other than the author (me). Essentially what's happening is that we are progressively zooming in on and object based on certain rules and conditions. Because the pattern is so regular, I have a hard time knowing the best way to document what's going on.

It would be great if someone with fresh eyes had a chance to read over this and help out. The best way to start IMO is with the initial "zooms". For example, the function lensToOperations above calls lensToPath, so lensToPath should be understood first before moving on to how it is used as a building bloc.

Once this is done, it'd be great to find a way to better document this code, which probably requires refactoring and perhaps even splitting into multiple files, so that it was more maintainable and ideally pedagogical.

Merge service.ts and serviceCore.ts

The separation of service.ts and serviceCore.ts used to make sense when service was used as a public wrapper around service core, but as the two evolved, they became pretty co-dependent and indistinguishable. I do not see any blockers to merging them, but there could be some distinctions between the two in the code base that need to be preserved.

It would be good to investigate how service and serviceCore are being called, note any meaningful distinctions, and either make these more apparent through better comments/interfaces or, if the distinctions do not warrant two separate files, merge the files.

Learnings from School of Startups workshop

Setting state multiple times can be a gotcha

Re-defining the state overwrites the previous state, so this does not work as expected:

githubv3.state(
  transform.withCodes(200),
);
githubv3.state(
  transform.responseBody({ lens: [Arr, 'private'] }).const(true),
);

Not sure if that's deliberate or not, anyway it might need some more clarification.

Usage in JavaScript can be awkward

This pattern loses type information:

describe("my test", () => {
  let github;
  beforeAll(() => {
    github = unmock.on().services.githubv3;
  });

With this pattern, variable github is typed as any in VS Code so the editor has no clue which methods are available. In TypeScript, this can be circumvented by typing the declaration as let github: IService.

Lot of boilerplate required

This is what my tests ended up like:

let githubv3;

describe('Fetching GitHub repositories', () => {
  beforeAll(() => {
    unmock.on();
    githubv3 = unmock.services.githubv3;
  });

  afterAll(() => {
    unmock.off();
  });

  beforeEach(() => {
    githubv3.state(transform.withCodes(200));
  });

  afterEach(() => {
    unmock.reset();
  });

  it('should return an array of repositories', async () => {
    const repos = await fetchGitHubRepos();
    expect(Array.isArray(repos)).toBe(true);
  });
});

Then I ended up copying the same boilerplate in another test suite. It could be reduced a bit by the following pattern:

let githubv3;

describe('Fetching GitHub repositories', () => {
  beforeAll(() => {
    unmock.on();
    githubv3 = unmock.services.githubv3;
  });

  afterAll(() => {
    unmock.off();
  });

  beforeEach(() => {
    unmock.reset();
  });

  it('should return an array of repositories', async () => {
    githubv3.state(transform.withCodes(200));
    const repos = await fetchGitHubRepos();
    expect(Array.isArray(repos)).toBe(true);
  });
});

In Jest, one could reduce boilerplate a bit by letting users set testEnvironment: "unmock-jest" or use a Jest preset (maybe not most handy as one can only define one).

Adding services can be (too) magical

Adding a service with yarn add -D @unmock/githubv3 is nice but is a bit too much magic. Maybe something like this would be more explicit:

import github from "@mocks/githubv3";  // Also a different name than @unmock?

unmock.register(github);

// No need for `let github;` declaration, `github` properly typed 

Add verbose option

Adding verbose: true might make it easier to show what unmock is doing behind the scenes. Alternatively, one should always install also the test reporter if that's not too much.

Unclear error message when yaml spec is wrong

It is really easy to write subtly wrong YAML and unmock will fail without any warning.

servers:
- https://example.prismic.io

Should be

servers:
- url: https://example.prismic.io

But unmock does not catch this and as a result, the tests fail later down the line with a cryptic error message.

Documentation from tutorial does not work

The last example in this tutorial does not work
https://www.unmock.io/hello.html

test("setting a value for endpoint", async () => {
  unmock.states().hello({ hello: "world" });
  var res = await axios.get("https://api.unmock.io");
  expect(res.data).toEqual({ hello: "world " });
});

First, there is a space on the last line:

expect(res.data).toEqual({ hello: "world " }); // Unnessary space after world

Then, it does not overwrite the response, the test fails, because the response is still "foo", not "world".

Missing a recorder/persister

Unmock is currently missing a recorder/persister like nock, yesno and pollyjs has. This would be a nice feature to have, especially if it were easy to use un conjunction with unmock.u.fuzz, which takes raw JSON data and creates a fuzzy version of it for fuzz testing (meaning that the structure is kept the same but dummy values are used).

Merge unmock-core to unmock-node

unmock-core was originally an isomorphic package usable both in Node.js and browser. The browser implementation is not needed anymore, so unmock-core can be merged to unmock-node inside, for example, core directory.

Also delete unmock-jsdom.

Property `method` in `UnmockRequest` should be enumerated

Property method is typed as string in UnmockRequest (alias for ISerializedRequest):

export interface ISerializedRequest {
  ...
  method: string;
  ...
}

This is a bit annoying when making assertions on the request object, as one needs to guess if the correct method is "get", "GET" or "Get". Change the typing to e.g. HTTPMethod from service/interfaces.ts (and add as export).

QUnit support

Currently, unmock hardcodes a lot of conventions for jest and will break on QUnit, especially the runner. Also, the jest-reporter will not work in QUnit. Everything should work in mocha as it does in Jest, with additional tests in mocha (probably in end-to-end) to make sure this is the case.

Specifying requestHeaders

Should be like specifying queries in the request - writes to parameters. We can use the nock syntax.

WIP RFC: Simplify architecture

Motivation

Current Unmock architecture feels a bit difficult to understand and hard to extend. Modifying Unmock requires, I think, rather intricate knowledge of the internals.

I think it should be possible to simplify the architecture to allow more easily extending it to different environments and to make it easier for new developers to understand.

Overview of current core components

Service

Describes an API service such as “GitHub”. A service essentially corresponds to a single OpenAPI specification. Exposes properties such as state, spy, and reset().

Delegates a lot of logic to ServiceCore. Strongly coupled to OpenAPI.

ServiceStore

Container for all services defined by the user or loaded from file system. Provides access to services via services property. Services can only be accessed by their "name", which is not always intuitively clear.

Faker

Matches serialized request to a corresponding service and generates response. Throws an error if no matching operation is found. Most of the mock generation logic is in generator.ts.

IInterceptor

Abstraction for intercepting (HTTP) requests and serving back mock data. Interceptor is responsible for serializing the request to ISerializedRequest object, passing it to Faker to generate ISerializedResponse and delivering a response. For an example, see NodeInterceptor and FetchInterceptor.

Backend

“Manager”-kind of class wrapping generic environment-unspecific behaviour.

UnmockPackage

Class implemented by unmock object exported by default from unmock-node and unmock-browser. Exposes options such as unmock.randomize.on(), services via unmock.services.githubv3, unmock.nock API for adding services programmatically, unmock.on() for switching on interceptor and loading services, etc.

ServiceDefLoader

Services are loaded when Backend is initialized. Currently this is a no-op in environments other than Node.js, where services are loaded from filesystem's __unmock__ folder.

Architecture diagram

Essentially looks like this.

Screenshot 2020-01-08 at 15 25 43

Requirements

WIP

  • Flexible interceptors.
  • Flexible service "origin"
  • Flexible mock generation

Suggestions for improvement

WIP

Provide access to services not by name but by their URL

Interceptor should accept a Faker or CreateResponse

More plugin-like architecture?

Get rid of Backend

Create an abstraction for services that could also work with GraphQL

Get rid of nock API

It feels very out-of-place, for example, in browser and React Native environments.

Typescript fails to compile when including unmock as a package dependency

Description

Unmock fails to compile because of a type import

Steps to Reproduce

  • Include unmock as a package dependency
  • Run tsc

Expected Result

Should compile.

Actual Result

Fails with this error:

node_modules/unmock-node/dist/backend.d.ts:2:37 - error TS2307: Cannot find module 'unmock-core/src/interceptor'.
2 import { IInterceptorFactory } from "unmock-core/src/interceptor";

Additional Context

To fix the bug, I'd recommend hoisting all imports to a top-level namespace. As the src directory is not packaged (only the dist one is), unmock-node cannot find unmock-core/src.

JSDoc strings missing for unmock

  • Development is hard when IDE does not explain methods such as unmock.reset().
  • Add doc strings for UnmockPackage and all its exposed methods

CLI

There's lots of boilerplate associated with unmock, including:

  • adding the reporter
  • setting up unmock in test files
  • pulling in OpenAPI specs

A CLI would help reduce this by (for example) adding the jest reporter, outputting a minimalistic test file with unmock in it, and listing available openapi specs on DefinitelyMocked. Basically, the idea would be a sort of create-unmock-app a la create-react-app, but with a better name

Mocha support in the runner

Currently, unmock hardcodes Jest error catching in the runner, which means that the runner will not work in mocha and other frameworks.

It would be great if the runner had conditionals for different frameworks, which would require testing different test failure cases for different packages and handling them in a similar manner.

The ugly thing about the way we do it is that we catch a JestAssertionError and treat that as the runner failing. JestAssertionError is an internal, undocumented way that jest causes tests to fail, so essentially we are creating a dependency on an internal API that could change in subsequent versions. It would be nice to address that to, and part of that could be asking the jest team to expose JestAssertionError in their external API so that it becomes a bit more sticky and less subject to change.

Serialization of fetch request needs implementation

unmock-fetch package contains a method for creating a "fake" fetch that's expected to implement the Fetch API. When a user creates a call with the fake fetch, the request should be serialized to an ISerializedRequest object. There's a crude implementation for serialize.ts that does not yet handle request headers or body.

I have written tests for the serialization features. They're skipped as they don't work yet, so the task would be to make those tests pass.

Note that the fetch interface also does not implement the case where "url" is not a plain string but a Request object. That would be a task for another PR.

In summary, we would need:

  • Serializing request headers (#347)
  • Filling "path" with the full path containing the search parameters
  • Serializing request body
  • Implementing serialization for the case where the input "url" is a Request object

Each of these could be a PR of their own.

Allow the path specification with an array

As per https://github.com/unmock/unmock-js/tree/new-readme#specifying-path.
Also, the more I think about it, the more the regex in the array should be a key-value pair so that the responseBody override should be able to reference it. So...

["id",["foo", /[123]+/],"bar"]

Instead of

["id", /[123]+/, "bar"]

The former allows for responseBody({ path: "/id/{foo}"}) whereas the latter does not. Of course, we should support the latter as well, in which case we just generate a random name for the path parameter (ie a uuid).

Using u.oneOf Breaks Unmock Response

Hi, I'm not sure if this is just a dumb question or if it should be a bug. I'm using version 0.3.16, and trying something like this to mock an Axios API in a React Native project:

import unmock, { u } from 'unmock';

unmock
  .nock("https://...", "myApi")
  .get("/cases")
  .reply(200, {
    status: "ok",
    cases: [{
      id: u.integer(),
      user_id: u.integer(),
      gender: u.oneOf("male", "female"),
      age: u.integer(),
      status: u.oneOf("current", "archived")
    }]);

The results coming back don't seem to be fully resolved into final values, for example:

  [
      {
        id: { type: 'integer', dynamic: 'ut' },
        user_id: { type: 'integer', dynamic: 'aute' },
        gender: { oneOf: 'male', dynamic: 'deserunt proident ad dolor cillum' },
        age: { type: 'integer', dynamic: 'proident' },
        status: { oneOf: 'current', dynamic: 'dolore quis ex dolor exercitation' },
      }
  ]

The test itself looks like this:

import React from 'react';
import App from '../App';
import { fireEvent, render, wait, waitForElement } from '@testing-library/react-native';
import unmock, { runner, transform, u } from 'unmock';

jest.mock('react-native-image-picker', () => ({ }));

beforeAll(() => {
  unmock.on();
  unmock
    .nock("https://peer-live-stage.nurelm.com/api", "peerApi")
    .get("/cases")
    .reply(200, {
      status: "ok",
      cases: [{
        id: u.integer(),
        user_id: u.integer(),
        gender: u.oneOf("male", "female"),
        age: u.integer(),
        status: u.oneOf("current", "archived"),
      }]
    //... plus a few more endpoints I left out to keep this short ...
    });
});

beforeEach(() => {
  unmock.reset();
});

describe('App Component', () => {
  describe('When a user trys to log in', () => {
    test('they go to live cases screen when the user/pass are valid', async () => {
      // Get the top level App element
      const { getByText, getByLabelText, getByTestId } = render(<App />);

      // Click the Login button on the onboarding screen
      const loginButton1 = await waitForElement(() => getByText('Login'));
      fireEvent.press(loginButton1);

      // Find user / pass fields and login button
      const [userField, passField, loginButton2] = await waitForElement(() => [
        getByLabelText('Email'),
        getByLabelText('Password'),
        getByText('Login')
      ])

      // Fill in the user/pass fields with silly text, tell Unmock to return
      // a valid response
      fireEvent.changeText(userField, 'SomeUsername');
      fireEvent.changeText(passField, 'SomePassword');
      fireEvent.press(loginButton2);

      // Look for "Live Cases" header
      const invalidLogin = await waitForElement(() => getByText(/cases/i));
    }, 30000);
  });

});

Thanks in advance for any tips, and for the great testing tool!

postRequestBody() returns a string '[object Object]'

Description

After making a call I check postRequestBody() and it returns a string.

Steps to Reproduce

      describe("when passing a body", () => {
        it("should be passed in request", async () => {
          await fetch(uri, { method: "POST", body });

           console.log(typeof endpoint.spy.postRequestBody()); // string
           console.log(endpoint.spy.postRequestBody()); // [object Object]
           expect(endpoint.spy.postRequestBody()).toBe(body); // fails
        });
      });

Expected Result

The object that I passed to fetch should be returned.

Actual Result

An irrelevant string is returned instead

Setup:

const body = {
  thisIs: "aBody",
};

beforeAll(() => {
  unmock.on();
  unmock
    .nock(url, "ENDPOINT")
    .get(path)
    .reply(200, returnedJson)
    .reply(400)
    .reply(501)
    .post(path)
    .reply(200, returnedJson)
    .reply(400)
    .reply(501);

  endpoint = unmock.services["ENDPOINT"];
});

afterAll(() => {
  unmock.off();
});

describe("fetch.js", () => {
  beforeEach(() => {
    unmock.reset();
  });
...
...

XMLHttpRequest Adapter

It would be great if unmock could be bundled via webpack and used in-browser for testing purposes.

Body in UnmockResponse is string when should be JSON

It's unexpected that body in UnmockResponse (alias for ISerializedResponse) is represented as string even when the response is application/json. Change it to work as UnmockRequest so that the body is string | object | undefined.

What is the right way to use responseBody().minItems(n)?

I've been trying to understand how to control the number of items coming back in an array. Here's an example:

unmock
  .nock('https://.../api', 'myApi')
  .get('/cases')
  .reply(200, {
    status: 'ok',
    id: u.integer(),
    cases: u.array({
      id: u.integer(),
      created_at: Date.now(),
      updated_at: Date.now()
    })
  });

The following does not change how many cases items are returned:

myApi.state(
  withCodes(200),
  responseBody().minItems(20)
);

Maybe it shouldn't work since I'm not using u.array at the top level of the response, in which case can anyone provide a tip on how to specify which response element you want to set minItems on?

Much thanks in advance!

Linting: Disallow imports from src/ directories in cross-package references

We recently realized (#383) that TypeScript fails to compile when including Unmock as a dependency if there are any imports from src/ directories.

In the case of #383 (fixed in #384), the src directory is not packaged (only the dist one is), unmock-node cannot find unmock-core/src. It'd be good to hoist all imports to a top-level namespace, and having a linting step to check for that would help catch any potential bugs.

Certain state keywords may fail

State keywords used in OpenAPI schema can cause the state management system to fail.
These include e.g. type, properties, additionalProperties, items, etc.

Sinon spies are awkward to work with

For example, we need to use stuff like (githubv3.spy.getCall(1).args[0].body as any) to treat the body as an object. Something a little shorter would be nice, ie githubv3.spy.getCall(1).requestJsonBody(). Basically the same thing we have as getRequestBody, deleteRequestBody except after the getCall.

JSBudapest - new & experimental features

Here are some themes that emerged from people trying out unmock during JS Budapest.

  • grpc?
  • graphql?
  • other protocols?
  • best way to onboard devs (koans, examples)?
  • commercial vs not?
  • redaction?
  • decentralized package management?

Admin endpoint in unmock-server

Add new HttpServer in unmock-server for setting state. This enables use cases where the mock server is running and user wants to add new services or modify their states. This issue does not cover adding such endpoints.

Missing README for unmock-browser

To add:

  • Add a note it intercepts fetch by overriding global.fetch
  • Does not intercept XMLHttpRequest (yet)

To decide:

  • Should users install unmock or unmock-browser? The first option would be simpler for documentation etc., but calling unmock.on() in development could then have undesired consequences (if the code runs also in the Node.js development server, unmock starts to intercept also there)
  • Should users use unmock-browser or just unmock-fetch? The latter does not export a "stand-alone" unmock package like unmock-node, unmock-browser and unmock do, should it?

Add an API for adding services from an OpenAPI schema

Description

When running unmock in the browser, the only way to define mock services is via the unmock.nock API. This works for small APIs, but cannot be used for large ones. If one has an OpenAPI schema available, for example, one should be able to define a service from the schema. In unmock-node this works via the __unmock__ folder, but that's not available in the browser.

Motivation

It would allow mocking big APIs in browser, where the file system is not available. It would also make the general architecture more modular, as FsServiceDefLoader reading services from the filesystem in Node.js could also use the API to add new services.

Example implementation

Something like this maybe:

const schema = require("./openapi.json");  // Bundlers can bundle the contents
import unmock, { Service } from "unmock";

unmock.add(Service.fromOpenAPI(schema));

// Now unmock can mock the API using the schema!

Using Faker:

const faker = unmock.faker();
faker.add(Service.fromOpenAPI(schema));
const res = faker.generate(reqToMyAPI);  // Should work

Improve the layout of the Jest Reporter

The Jest Reporter is missing a few cosmetic tweaks, such as:

  • missing pointer over clickable regions
  • border around selected regions for the colorblind/visually impaired
  • if there are a lot of tests, it is not clear that the selected test corresponds to the log on the bottom of the page. For 1-2 tests this is clear, but as soon as you have to scroll, it is tough to navigate.

Extract types from unmock-core to a new package

It would be very useful to extract common types from interfaces.ts to a separate lerna package called, for example, unmock-types. Once this is done, one could remove for example unmock-core dependency from unmock-fetch and only depend on unmock-types. Types could then also be used in other projects.

What should be moved:

  • ISerializedRequest and ISerialzedResponse and everything they depend on
  • OnSerializedRequest and CreateResponse

How to create a new package:

  1. Copy an existing (light-weight) package such as unmock-jest to a new package called unmock-types
  2. Update package.json
  3. Update tsconfig.json and its references in the new package
  4. Remove everything from src/ except index.ts and src/__tests__/tsconfig.json
  5. Update tsconfig.build.json in the project root to refer to the package
  6. Update tsconfig.test.json
  7. Add the package as a dependency in the root package.json
  8. Add the package to lint-ts and lint-ts-fix commands in package.json

Instead of copying, you can also use the lerna CLI's create command to create the package.

For this issue, it's not required to update other packages to use the new package. That can (and should) be done in later PRs.

More human-friendly CONTRIBUTING.md

While we do have a CONTRIBUTING.md... it could use some love. Right now, it's a bit dry and super long. It'd be great to be able to break up some of that information to make it more digestible and immediately potential useful to contributors!

Some section ideas...

  • A thank you to potential contributors
  • Table of Contents
  • Code of Conduct
  • What people should know before getting started
  • How people can contribute
  • Styleguides
  • Pull request process
  • Questions/getting in touch

Some projects for inspo:

💖 Feel free to ask questions and be creative 💖

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.