Skip to content

Commit

Permalink
Update stubs.md
Browse files Browse the repository at this point in the history
Proposed improvement to stubs doc, [as suggested here](sinonjs#2062).

- Focus more on the basics of stubbing an object's function (stub pre-programmed to throw exception/return value)
- Less discussion on spies
- Provide very simple code to test as an example, instead of external library's (PubSub) code
- Show why restoration of a stub is necessary
  • Loading branch information
andrewdroz committed Nov 5, 2019
1 parent edc98b7 commit bc27118
Showing 1 changed file with 76 additions and 19 deletions.
95 changes: 76 additions & 19 deletions docs/release-source/release/stubs.md
Expand Up @@ -16,36 +16,95 @@ wrapping an existing function with a stub, the original function is not called.

### When to use stubs?

Use a stub when you want to:
Stubs are useful when we need to test a module without calling its dependency. They replace the dependency and simulate it by returning a desired value. Use a stub when you want to:

1. Control a method's behavior from a test to force the code down a specific path. Examples include forcing a method to throw an error in order to test error handling.

2. When you want to prevent a specific method from being called directly (possibly because it triggers undesired behavior, such as a `XMLHttpRequest` or similar).

The following example is yet another test from [PubSubJS][pubsubjs] which shows how to create an anonymous stub that throws an exception when called.
Stubs have methods that can add custom behaviors including:
- returning a specific value
- throwing a specified exception
- automatically invoking callbacks with provided arguments
- defining behavior on nth call to stub

In the example below, an 'orderService' has a dependency with an 'emailService'. This example's objective is to stub the email service's 'send' function.

```javascript
"test should call all subscribers, even if there are exceptions" : function(){
var message = 'an example message';
var stub = sinon.stub().throws();
var spy1 = sinon.spy();
var spy2 = sinon.spy();
const emailService = require('./emailService');

let orderService = {
confirm() {
let isEmailSent;
try {
isEmailSent = emailService.send();
} catch (EmailClientError) {
return 'Error occurred in email client!';
}

PubSub.subscribe(message, stub);
PubSub.subscribe(message, spy1);
PubSub.subscribe(message, spy2);
if (isEmailSent) return 'Email was sent!';
else return 'Internal email service failed!';
}
}
```

PubSub.publishSync(message, undefined);
Without writing the code for its dependency, the 'orderService' can be unit-tested. The only requirement is for 'emailService.js' file to have a 'send' property:

assert(spy1.called);
assert(spy2.called);
assert(stub.calledBefore(spy1));
```javascript
module.exports = {
send() {}
}
```

Note how the stub also implements the spy interface. The test verifies that all
callbacks were called, and also that the exception throwing stub was called
before one of the other callbacks.
A test can be written for exception handling. To do that, a stub pre-programmed to throw the exception must be provided in place of the dependency's actual function:

```javascript
describe('orderService', function() {
it('should handle EmailClientError when emailService throws', function() {

// SETUP
sinon.stub(emailService, 'send').throws('EmailClientError');
// EXERCISE
let result = orderService.confirm();
// VERIFY
expect(result).toBe("Error occurred in email client!");
});
});
```

A stub can also be provided with the behaviour to return some value. Here, the stub will return true when called, in order to trigger a particular code path within 'orderService':

```javascript
describe('orderService', function() {
it('should return confirmation string when emailService successfully sends', function() {

// SETUP
sinon.stub(emailService, 'send').returns(true);
// EXERCISE
let result = orderService.confirm();
// VERIFY
expect(result).toBe("Email was sent!");
});
});
```

The 2 tests above pass when run in isolation. However, when these tests are run in a single test suite, the second test would fail because the function was already stubbed. In order to change the stub for the same function, the stub needs to be restored:

```javascript
describe('orderService', function() {
it('should handle EmailClientError when emailService throws', function() {

// SETUP
let sendStub = sinon.stub(emailService, 'send').throws('EmailClientError');
// EXERCISE
let result = orderService.confirm();
// VERIFY
expect(result).toBe("Error occurred in email client!");
// TEARDOWN
sendStub.restore();
});
});
```

### Defining stub behavior on consecutive calls

Expand All @@ -59,8 +118,6 @@ and `callsArg*` family of methods define a sequence of behaviors for consecutive
calls. As of 1.8, this functionality has been removed in favor of the
[`onCall`](#stuboncalln-added-in-v18) API.

[pubsubjs]: https://github.com/mroderick/pubsubjs

### Stub API

### Properties
Expand Down

0 comments on commit bc27118

Please sign in to comment.