dce-stub
Version:
A simple way to stub default-only dependencies.
229 lines (178 loc) • 5.04 kB
Markdown
# dce-stub
A simple way to stub default-only dependencies. This project currently only works for stubbing the default export of a module.
We utilize dependency injection, so stubbed modules cannot be used until time of test (not at import time). For more info, see step 4 in the instructions below.
## Usage
### 1. Import `dce-stub`
```js
import runStubbed from 'dce-stub';
```
### 2. Import the dependencies you wish to stub using * syntax
In your spec:
```js
import * as myDependency from '...';
...
```
In the module you are testing:
```js
import * as myDependency from '...';
...
// When using the dependency, use the .default property:
myDependency.default...
...
```
### 3. Run your tests within the `runStubbed` function
Call `runStubbed` with two arguments:
- **replacements** `object|object[]` – either one or many stub replacements, each of the form `{ dep, stub }` where `dep` is the dependency and `stub` is the stubbed version of the dependency
- **test** `function` – tests to run while the dependency is stubbed (dependencies are restored once the function finishes). Function may be asynchronous or synchronous
```js
describe('My Feature', () => {
it('Does Something', async () => {
...
// Define stub replacement
const replacement = {
dep: myDependency,
stub: <stub of myDependency>,
};
// Run the
await runStubbed(replacement, async () => {
...
});
});
});
```
If you have more than one dependency to stub, just use a list:
```js
...
// Define stub replacements
const replacements = [
{
dep: myDependency,
stub: <stub of myDependency>,
},
{
dep: secondDepencency,
stub: <stub of secondDependency>,
},
...
];
// Run the
await runStubbed(replacements, async () => {
...
});
```
### 4. Configure/use dependencies at time of use
We use dependency injection, which means that the stubbed version of the dependency is _not_ injected at import time. Instead, it is injected when the test runs.
**Don't configure/initialize/use stubbed libraries until time of test:**
Example: we are using a library `lms` that must be initialized before being used.
Wrong way:
> ```js
> import * as lms from 'my-lms';
>
> const api = lms.default.getAPI();
>
> class Course {
> constructor(id) {
> this.id = id;
> }
>
> listStudents() {
> return api.course.listStudents(this,id);
> }
>
> ...
> }
> ```
Right way:
> ```js
> import * as lms from 'my-lms';
>
> class Course {
> constructor(id) {
> this.id = id;
>
> this.api = lms.getAPI();
> }
>
> listStudents() {
> return this.api.course.listStudents(this,id);
> }
>
> ...
> }
> ```
Another right way:
_(if there is no cost to re-initializing over and over)_
> ```js
> import * as lms from 'my-lms';
>
> class Course {
> constructor(id) {
> this.id = id;
> }
>
> listStudents() {
> const api = lms.getAPI();
> return this.api.course.listStudents(this,id);
> }
>
> ...
> }
> ```
**Note:** One concrete example of such a library that can be re-initialized with no cost, is `caccl/client/cached`
## Example
We have two helpers:
- `getNameFromServer` – pulls the current user's first name from the server
- `genIntro` – a script that calls `getNameFromServer` and generates a welcome message
Our goal is to write a unit test for `genIntro`, isolating it from `getNameFromServer`. Thus, we want to import `genIntro` while replacing `getNameFromServer` with a fake stubbed version of that module.
### `getNameFromServer.js`
```js
export default async () => {
return sendRequest({
method: 'GET',
url: 'https://host.com/user/profile/name',
});
};
```
### `genIntro.js`
```js
import getNameFromServer from './getNameFromServer';
export default async () => {
// Get the current user's name
const name = await getNameFromServer();
// Create the message
return `Hi ${name}! It is a pleasure to meet you.`;
};
```
### `genIntro.spec.js`
```js
import runStubbed from 'dce-stub';
// Import module to test
import genIntro from './genIntro';
// Import dependencies we want to stub
import * as getNameFromServer from './getNameFromServer';
// Create the getNameFromServer stub
const getNameFromServerStub = () => {
return 'Divardo';
};
// Tests
describe('genIntro', () => {
it('Generates a valid introduction', async () => {
// Create stub replacements
const replacement = {
dep: getNameFromServer,
stub: getNameFromServerStub,
};
// Run tests with stub replacements
await runStubbed(replacement, async () => {
// Generate an intro message
const intro = await genIntro();
// Test the intro
assert.equal(
intro,
'Hi Divardo! It is a pleasure to meet you.',
'Invalid intro produced'
);
});
});
});
```