-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from MarcL/day5-new
Day 5 : GitHub Stargazing with Spies & Stubs
- Loading branch information
Showing
5 changed files
with
259 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
# Day 5 notes - GitHub stargazing with spies and stubs | ||
|
||
## What you'll do | ||
|
||
Today you're going to look at calling the GitHub API using a HTTP request | ||
|
||
## Install request-promise | ||
|
||
You're going to make a HTTP request to the GitHub API using `request-promise`. This is a simple HTTP request client with Promise support. Install `request-promise` (and also `request` as it is a peer dependency) using `npm`: | ||
|
||
```shell | ||
npm install --save request request-promise | ||
``` | ||
|
||
## Spies vs Stubs | ||
|
||
Spies and stubs are sometimes confused but they serve two different purposes. **Spies** live up to their name and the spy on functions but don't alter the function's behaviour at all. We can use them to tell us that something has been called and how. **Stubs** are similar to spies, and they support the sinon spy API, but they also alter the behaviour of the function. This means you can define how the function will respond. This allows you to better test the happy and unhappy paths in your code. | ||
|
||
### Create the code | ||
|
||
Create a new module in the `src` directory called `day5.js` and corresponding test file in the `test` directory called `day5.test.js`: | ||
|
||
```javascript | ||
import requestPromise from 'request-promise'; | ||
|
||
function day5(owner, repository) { | ||
}); | ||
|
||
export default day5; | ||
``` | ||
|
||
```javascript | ||
import {expect} from 'chai'; | ||
import sinon from 'sinon'; | ||
import requestPromise from 'request-promise'; | ||
import day5 from '../src/day5'; | ||
|
||
describe('day5 tests', () => { | ||
}); | ||
``` | ||
|
||
### Spy on the GitHub API | ||
|
||
Let's spy on the GitHub API request and confirm that it's called once. Set up the spy to inspect `requestPromise.get` for the GET request we'll make to the API. You can then write a test that confirms that the spy was called only once. | ||
|
||
```javascript | ||
let spyRequestGet; | ||
|
||
beforeEach(() => { | ||
spyRequestGet = sinon.spy(requestPromise, 'get'); | ||
}); | ||
|
||
afterEach(() => { | ||
spyRequestGet.restore(); | ||
}); | ||
|
||
it('should call the expected endpoint once', () => { | ||
return day5() | ||
.then(() => { | ||
expect(spyRequestGet.callCount).to.equal(1); | ||
}); | ||
}); | ||
``` | ||
|
||
And write the code to pass this: | ||
|
||
```javascript | ||
function day5(owner, repository) { | ||
const gitHubRepoGetUrl = | ||
`https://api.github.com/repos/${owner}/${repository}`; | ||
|
||
const requestOptions = { | ||
uri: gitHubRepoGetUrl, | ||
resolveWithFullResponse: true, | ||
json: true, | ||
headers: { | ||
'User-Agent': 'JavaScript Testing Beginners Course' | ||
} | ||
}; | ||
|
||
// We need a few extra parameters | ||
return requestPromise.get(requestOptions); | ||
} | ||
``` | ||
|
||
### Confirm the correct API endpoint | ||
|
||
Let's write a test to confirm that the correct endpoint has been called. | ||
|
||
```javascript | ||
it('should call the expected endpoint url', () => { | ||
const expectedGitHubUrl = 'https://api.github.com/repos/expressjs/express'; | ||
return day5('expressjs', 'express') | ||
.then((data) => { | ||
expect(spyRequestGet.getCall(0).args[0].uri) | ||
.to.equal(expectedGitHubUrl); | ||
}); | ||
}); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,5 +23,8 @@ | |
"chai": "~4.1.1", | ||
"mocha": "~3.5.0", | ||
"sinon": "~3.2.1" | ||
}, | ||
"dependencies": { | ||
"request": "~2.81.0" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import request from 'request'; | ||
|
||
function day5(owner, repository, callback) { | ||
const gitHubUrl = `https://api.github.com/repos/${owner}/${repository}`; | ||
|
||
const requestOptions = { | ||
uri: gitHubUrl, | ||
headers: { | ||
'User-Agent': 'JavaScript Testing For Beginners' | ||
}, | ||
resolveWithFullResponse: true, | ||
json: true | ||
}; | ||
|
||
request.get(requestOptions, (error, response, body) => { | ||
if (response.statusCode == 403) { | ||
callback({ | ||
success: false, | ||
statusCode: response.statusCode, | ||
error: 'API is rate limited - try again later' | ||
}) | ||
} else { | ||
callback({ | ||
stars: body.stargazers_count | ||
}); | ||
} | ||
}); | ||
} | ||
|
||
export default day5; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import {expect} from 'chai'; | ||
import sinon from 'sinon'; | ||
import request from 'request'; | ||
import day5 from '../src/day5'; | ||
|
||
describe('day5 tests', () => { | ||
|
||
describe('using spies', () => { | ||
let spyRequestGet; | ||
|
||
beforeEach(() => { | ||
spyRequestGet = sinon.spy(request, 'get'); | ||
}); | ||
|
||
afterEach(() => { | ||
spyRequestGet.restore(); | ||
}); | ||
|
||
// Should make 1 GET request | ||
it('should make a get request once', (done) => { | ||
day5('expressjs', 'express', (data) => { | ||
expect(spyRequestGet.callCount) | ||
.to.equal(1); | ||
|
||
done(); | ||
}) | ||
}); | ||
|
||
// Should make request with expected URL | ||
it('should make request with expected URL', (done) => { | ||
day5('expressjs', 'express', (data) => { | ||
expect(spyRequestGet.getCall(0).args[0].uri) | ||
.to.equal('https://api.github.com/repos/expressjs/express'); | ||
done(); | ||
}) | ||
}); | ||
}); | ||
|
||
describe('using stubs', () => { | ||
let stubRequestGet; | ||
|
||
before(() => { | ||
// before the test suite | ||
}); | ||
|
||
after(() => { | ||
// after the test suite | ||
}); | ||
|
||
beforeEach(() => { | ||
// before each test | ||
stubRequestGet = sinon.stub(request, 'get'); | ||
}); | ||
|
||
afterEach(() => { | ||
// after each test | ||
stubRequestGet.restore(); | ||
}); | ||
|
||
// Should make 1 request | ||
it('should make one GET request', (done) => { | ||
stubRequestGet.yields( | ||
null, | ||
{statusCode: 200}, | ||
{stargazers_count: 1000} | ||
); | ||
|
||
day5('expressjs', 'express', (data) => { | ||
expect(stubRequestGet.callCount) | ||
.to.equal(1); | ||
|
||
done(); | ||
}); | ||
}); | ||
|
||
// Should return correct data | ||
it('should return expected data', (done) => { | ||
const givenApiResponse = { | ||
'stargazers_count': 100 | ||
}; | ||
|
||
stubRequestGet.yields( | ||
null, | ||
{statusCode: 200}, | ||
givenApiResponse | ||
); | ||
|
||
day5('expressjs', 'express', (data) => { | ||
expect(data).to.deep.equal({ | ||
stars: givenApiResponse.stargazers_count | ||
}); | ||
done(); | ||
}) | ||
}); | ||
|
||
// Should return correct data when error | ||
it('should return expected data when rate limited', (done) => { | ||
const givenApiResponse = { | ||
'message': 'API rate limit exceeded', | ||
'documentation_url': 'https://developer.github.com/v3/#rate-limiting' | ||
}; | ||
|
||
stubRequestGet.yields( | ||
new Error('API rate limit exceeded'), | ||
{statusCode: 403}, | ||
givenApiResponse | ||
); | ||
|
||
day5('expressjs', 'express', (data) => { | ||
expect(data).to.deep.equal({ | ||
success: false, | ||
statusCode: 403, | ||
error: 'API is rate limited - try again later' | ||
}); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); |