Challenge #9: Distinct Responses
Some services will require the same endpoint to be called multiple times, returning a different response each time. In this challenge, you’ll update your embedded stub to support that type of behavior, but you’ll also continue to support the existing behavior:
If an endpoint is configured with an array, it will return the next response in the array each time a response is needed. Otherwise, if it’s configured with a single response (that’s not in an array), it will always return that response.
Instructions
1. In the "provides distinct responses when an endpoint has a list of responses configured"
test:
- Configure the Nulled
HttpClient
to return multiple response, as follows:const client = HttpClient.createNull({ "/endpoint": [ { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" }, { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" }, ], });
- Make multiple requests of
/endpoint
and assert that the responses match the configured values.
2. In the production code:
- Modify the embedded stub to make the test pass.
Remember to commit your changes when you’re done.
API Documentation
const isArray = Array.isArray(variable)
Returns true
if variable
is an array and false
if it isn’t.
- variable
(any)
- the variable to test - returns isArray
(boolean)
- whether the variable is an array
const element = array.shift()
Gets the first element from an array and removes it from the array.
- array
(array)
- the array to modify - returns element
(boolean)
- the first element in the array
JavaScript Primers
No new concepts.
Hints
1
The test needs to configure the responses, then make multiple requests and assertions.
This is like the previous test.
it("provides distinct responses when an endpoint has a list of responses configured", async () => {
const client = HttpClient.createNull({
"/endpoint": [
{ status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
{ status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
],
});
const { response: response1 } = await requestAsync({ client, path: "/endpoint" });
const { response: response2 } = await requestAsync({ client, path: "/endpoint" });
assert.deepEqual(response1, {
status: 200,
headers: { myheader1: "myValue1" },
body: "my body 1",
});
assert.deepEqual(response2, {
status: 300,
headers: { myheader2: "myValue2" },
body: "my body 2",
});
});
2
The test is ready to run.
It should fail, saying that status
, headers
, and body
have the wrong values.
That’s because the stub is expecting an object, but the configuration is passing in an array.
3
StubbedRequest
is no longer being configured with a single response.
It needs to get the next response from the list.
You can use this._responses.shift()
to do that.
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
setImmediate(() => {
const response = this._responses.shift();
this.emit("response", new StubbedResponse(response));
});
}
}
This code won’t compile in TypeScript. The reason is discussed in the next hint.
4
In TypeScript, the code doesn’t compile. (If you’re using JavaScript, skip this hint.)
It should complain that this._responses
is possibly undefined
and that shift()
does not exist.
The shift()
error is the bigger issue.
It’s happening because StubbedHttp
is using as NulledHttpClientResponse
to hide the response’s true type, which is NulledHttpClientResponse | NulledHttpClientResponse[]
.
Remove the as
clause and fix the types.
class StubbedHttp {
constructor(private readonly _responses: NulledHttpClientResponses = {}) {
}
request({ path }: { path: string }) {
return new StubbedRequest(this._responses[path]); // removed "as NulledHttpClientResponse"
}
}
class StubbedRequest extends EventEmitter {
constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
super();
}
end() {
setImmediate(() => {
const response = this._responses.shift();
this.emit("response", new StubbedResponse(response));
});
}
}
5
In TypeScript, the code still doesn’t compile. (If you’re using JavaScript, skip this hint.)
It’s complaining for the same reasons, even though you added declared that this._responses
could be an array.
But it’s not guaranteed to be an array.
The code still won’t compile because shift()
is only available on arrays, and this._responses
isn’t guaranteed to be an array.
6
In JavaScript, what happens if you run the tests? (If you’re using TypeScript, skip this hint.)
They should fail, complaining that this._responses.shift()
is not a function. Why do they fail?
Look at the test names.
It’s the other nulled instance
tests that are failing. The test you’re working on passed.
The tests are failing because their responses
aren’t an array.
7
Modify the code to check if this._responses
is an array.
You can use Array.isArray()
for that.
If it’s not an array, you can use it as-is.
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
setImmediate(() => {
let response;
if (Array.isArray(this._responses)) {
response = this._responses.shift();
}
else {
response = this._responses;
}
this.emit("response", new StubbedResponse(response));
});
}
}
8
Refactor and clean up.
This is a matter of taste, with no right answer.
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
setImmediate(() => {
const response = nextResponse(this._responses);
this.emit("response", new StubbedResponse(response));
});
}
}
function nextResponse(responses) {
if (Array.isArray(responses)) {
return responses.shift();
}
else {
return responses;
}
}
TypeScript’s implementation of nextResponse()
needs a type declaration:
function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
if (Array.isArray(responses)) {
return responses.shift();
}
else {
return responses;
}
}
Complete Solution
Test code:
it("provides distinct responses when an endpoint has a list of responses configured", async () => {
const client = HttpClient.createNull({
"/endpoint": [
{ status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
{ status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
],
});
const { response: response1 } = await requestAsync({ client, path: "/endpoint" });
const { response: response2 } = await requestAsync({ client, path: "/endpoint" });
assert.deepEqual(response1, {
status: 200,
headers: { myheader1: "myValue1" },
body: "my body 1",
});
assert.deepEqual(response2, {
status: 300,
headers: { myheader2: "myValue2" },
body: "my body 2",
});
});
Production code (JavaScript):
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
setImmediate(() => {
const response = nextResponse(this._responses);
this.emit("response", new StubbedResponse(response));
});
}
}
function nextResponse(responses) {
if (Array.isArray(responses)) {
return responses.shift();
}
else {
return responses;
}
}
Production code (TypeScript):
class StubbedHttp {
constructor(private readonly _responses: NulledHttpClientResponses = {}) {
}
request({ path }: { path: string }) {
return new StubbedRequest(this._responses[path]);
}
}
class StubbedRequest extends EventEmitter {
constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
super();
}
end() {
setImmediate(() => {
const response = nextResponse(this._responses);
this.emit("response", new StubbedResponse(response));
});
}
}
function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
if (Array.isArray(responses)) {
return responses.shift();
}
else {
return responses;
}
}