Challenge #5: Configurable Endpoints
Your high-level infrastructure will often support more than one HTTP endpoint. In this challenge, you’ll update your embedded stub to support custom responses for each endpoint.
Instructions
1. Modify the "can be configured with a different response per endpoint"
test:
- Replace the Nulled
HttpClient
with the following endpoint-specific configuration:const client = HttpClient.createNull({ "/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" }, "/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" }, });
- Assert that each endpoint returns the corresponding response.
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 { response } = await requestAsync({ client, path });
Available in the tests. A helper method for calling httpClient.requestAsync()
. Its main benefit is that it provides defaults for parameters your test doesn’t care about.
- optional client
(HttpClient)
- theHttpClient
to use; defaults toHttpClient.create()
- optional path
(HttpClient)
- the path to use; defaults to"/irrelevant/path"
- returns response
({ status, headers, body })
- the HTTP response - returns status
(number)
- the response status code - returns headers
(object)
- the response headers - returns body
(string)
- the response body
JavaScript Primers
Hints
1
Update the test.
It’s just a bigger version of the existing test. Remember to update the assertions.
You’ll need to rename the destructured variables.
it("can be configured with a different response per endpoint", async () => {
const client = HttpClient.createNull({
"/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
"/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
});
const { response: response1 } = await requestAsync({ client, path: "/endpoint/1" });
const { response: response2 } = await requestAsync({ client, path: "/endpoint/2" });
assert.deepEqual(response1, {
status: 200,
headers: { myheader1: "myValue1" },
body: "my body 1",
});
assert.deepEqual(response2, {
status: 300,
headers: { myheader2: "myValue2" },
body: "my body 2",
});
});
The TypeScript type declaration in HttpClient.createNull()
was a placeholder, and can now be simplified:
static createNull(responses?: NulledHttpClientResponses): HttpClient {
return new HttpClient(new StubbedHttp(responses));
}
2
The test is ready to run.
It should fail, saying that the status
, headers
, and body
are undefined
. (In TypeScript, they’ll have placeholder values.)
That’s because you’ve introduced the endpoints, so the top-level object doesn’t have status
, headers
, and body
properties any more. Instead, the top-level object has /endpoint/1
and /endpoint/2
properties.
3
How can you make the production code pass?
StubbedResponse
needs to have access to the correct response.
What is the “correct” response?
It’s the one that matches the path being requested.
How do you know which path is being requested?
Trace through the production code to find out.
The path
is being passed to this._http.request()
.
The corresponding stub is StubbedHttp.request()
.
You can make the production code pass by modifying StubbedHttp.request()
. It will use the path
parameter to pull the correct response out of the configured responses.
4
Modify the embedded stub to get the correct response.
Modify StubbedHttp.request()
to get the path.
You can do that with object destructuring.
When you have the path, you can use it to dereference the configured responses.
class StubbedHttp {
constructor(responses) {
this._responses = responses;
}
request({ path }) {
return new StubbedRequest(this._responses[path]);
}
}
In TypeScript, you’ll need to change the type of _responses
. You’ll also need to declare that the response isn’t an array. (You’ll add support for arrays later.)
class StubbedHttp {
constructor(private readonly _responses?: NulledHttpClientResponses) {
}
request({ path }: { path: string }) {
return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
}
}
TypeScript will still complain about a possibly undefined
value, but that’s discussed in the next hint.
5
In TypeScript, why doesn’t the code compile? (If you’re using JavaScript, skip this hint.)
It should be complaining that this._responses
is possibly undefined
.
Where does this._responses
come from?
The code isn’t compiling because it isn’t handling the case where no responses have been provided.
6
In JavaScript, what happens if you run the tests? (If you’re using TypeScript, skip this hint.)
They should fail, complaining about undefined
while reading /irrelevant/path
. Why do they fail?
Look at the test names.
It’s the other nulled instance
tests that are failing.
Look at the stack trace.
The error is occurring on this line: return new StubbedRequest(this._responses[path]);
.
The tests are failing because they don’t provide responses
.
7
Fix the tests (in JavaScript) or compile error (in TypeScript).
You need to provide a default response.
You can do that with a default parameter.
class StubbedHttp {
constructor(responses = {}) {
this._responses = responses;
}
request({ path }) {
return new StubbedRequest(this._responses[path]);
}
}
In TypeScript, it looks like this:
class StubbedHttp {
constructor(private readonly _responses: NulledHttpClientResponses = {}) {
}
request({ path }: { path: string }) {
return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
}
}
Complete Solution
Test code:
it("can be configured with a different response per endpoint", async () => {
const client = HttpClient.createNull({
"/endpoint/1": { status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
"/endpoint/2": { status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
});
const { response: response1 } = await requestAsync({ client, path: "/endpoint/1" });
const { response: response2 } = await requestAsync({ client, path: "/endpoint/2" });
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 StubbedHttp {
constructor(responses = {}) {
this._responses = responses;
}
request({ path }) {
return new StubbedRequest(this._responses[path]);
}
}
Production code (TypeScript):
class StubbedHttp {
constructor(private readonly _responses: NulledHttpClientResponses = {}) {
}
request({ path }: { path: string }) {
return new StubbedRequest(this._responses[path] as NulledHttpClientResponse);
}
}