Challenge #4: Configurable Responses
Typically, someone using HttpClient
is going to have specific responses they’re looking for. This is implemented with the Configurable Responses pattern. In this challenge, you’ll modify your embedded stub to support configuring a single, generic response. That’s not sufficient for real-world work, but it’s a good starting point.
Instructions
1. In the "can be configured with a different response per endpoint"
test:
- Configure the Nulled
HttpClient
to return a predefined response, as follows:const client = HttpClient.createNull({ status: 200, headers: { myheader: "myValue" }, body: "my body" });
- Assert that it returns that 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
No new methods.
TypeScript Types
HttpClient.createNull(responses: NulledHttpClientResponses)
HttpClient
’s configurable responses.
type NulledHttpClientResponses = Record<string, NulledHttpClientResponse | NulledHttpClientResponse[]>;
interface NulledHttpClientResponse {
status?: number,
headers?: HttpHeaders,
body?: string,
hang?: boolean,
}
type HttpHeaders = Record<string, HttpHeader>;
type HttpHeader = string;
JavaScript Primers
Hints
1
Implement the test.
It’s very similar to the previous test.
it("can be configured with a different response per endpoint", async () => {
const client = HttpClient.createNull({
status: 200,
headers: { myheader: "myValue" },
body: "my body"
});
const { response } = await requestAsync({ client });
assert.deepEqual(response, {
status: 200,
headers: { myheader: "myValue" },
body: "my body",
});
});
2
The test is ready to run.
It should fail, saying that the status
, headers
, and body
are different than expected.
That’s because the embedded stub isn’t using the configured response.
3
You need to get the configured response to the place where it’s needed.
Where does it come from?
It comes from HttpClient.createNull()
.
Where is it needed?
It’s needed where the DEFAULT_NULLED_RESPONSE
is used.
It’s needed in the StubbedResponse
constructor.
The data needs to follow this path: HttpClient.createNull()
→ StubbedHttp
→ StubbedRequest
→ StubbedResponse
.
export class HttpClient {
// ...
static createNull(responses) {
return new HttpClient(new StubbedHttp(responses));
}
// ...
}
class StubbedHttp {
constructor(response) {
this._response = response;
}
request() {
return new StubbedRequest(this._response);
}
}
class StubbedRequest extends EventEmitter {
constructor(response) {
super();
this._response = response;
}
end() {
setImmediate(() => {
this.emit("response", new StubbedResponse(this._response));
});
}
}
class StubbedResponse extends EventEmitter {
constructor(response) {
super();
this.statusCode = DEFAULT_NULLED_RESPONSE.status;
this.headers = DEFAULT_NULLED_RESPONSE.headers;
setImmediate(() => {
this.emit("data", DEFAULT_NULLED_RESPONSE.body);
this.emit("end");
});
}
}
TypeScript will need type declarations:
class StubbedHttp {
constructor(private readonly _response?: NulledHttpClientResponse) {
}
request() {
return new StubbedRequest(this._response);
}
}
class StubbedRequest extends EventEmitter {
constructor(private readonly _response?: NulledHttpClientResponse) {
super();
}
end() {
setImmediate(() => {
this.emit("response", new StubbedResponse(this._response));
});
}
}
class StubbedResponse extends EventEmitter implements NodeHttpResponse {
statusCode: number;
headers: HttpHeaders;
constructor(response?: NulledHttpClientResponse) {
super();
this.statusCode = DEFAULT_NULLED_RESPONSE.status;
this.headers = DEFAULT_NULLED_RESPONSE.headers;
setImmediate(() => {
this.emit("data", DEFAULT_NULLED_RESPONSE.body);
this.emit("end");
});
}
}
4
Now that the response is in the right place, you need to return it.
You can replace the default response. (You’ll put it back in a moment.)
class StubbedResponse extends EventEmitter {
constructor(response) {
super();
this.statusCode = response.status;
this.headers = response.headers;
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
TypeScript will require placeholders and special handling for undefined
:
class StubbedResponse extends EventEmitter implements NodeHttpResponse {
statusCode: number;
headers: HttpHeaders;
constructor(response?: NulledHttpClientResponse) {
super();
this.statusCode = response?.status ?? 42;
this.headers = response?.headers ?? {};
setImmediate(() => {
this.emit("data", response?.body);
this.emit("end");
});
}
}
5
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 status
. 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: this.statusCode = response.status;
.
The tests are failing because they don’t provide a response
.
6
In TypeScript, what happens if you run the tests? (If you’re using JavaScript, skip this hint.)
They should fail, complaining that the default response isn’t being returned. Why do they fail?
Look at the test names.
It’s the provides default response
test that is failing.
The test is failing because it doesn’t provide a response
.
7
Fix the tests.
You need to provide a default response.
You can do that with a default function parameter.
class StubbedResponse extends EventEmitter {
constructor(response = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status;
this.headers = response.headers;
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
In TypeScript, you can remove the ?
operators.
class StubbedResponse extends EventEmitter implements NodeHttpResponse {
statusCode: number;
headers: HttpHeaders;
constructor(response: NulledHttpClientResponse = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status ?? 42;
this.headers = response.headers ?? {};
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
Complete Solution
Test code:
it("can be configured with a different response per endpoint", async () => {
const client = HttpClient.createNull({
status: 200,
headers: { myheader: "myValue" },
body: "my body"
});
const { response } = await requestAsync({ client });
assert.deepEqual(response, {
status: 200,
headers: { myheader: "myValue" },
body: "my body",
});
});
Production code (JavaScript):
export class HttpClient {
// ...
static createNull(responses) {
return new HttpClient(new StubbedHttp(responses));
}
// ...
}
class StubbedHttp {
constructor(response) {
this._response = response;
}
request() {
return new StubbedRequest(this._response);
}
}
class StubbedRequest extends EventEmitter {
constructor(response) {
super();
this._response = response;
}
end() {
setImmediate(() => {
this.emit("response", new StubbedResponse(this._response));
});
}
}
class StubbedResponse extends EventEmitter {
constructor(response = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status;
this.headers = response.headers;
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}
Production code (TypeScript):
export class HttpClient {
// ...
static createNull(
responses?: NulledHttpClientResponse | NulledHttpClientResponses
): HttpClient {
return new HttpClient(new StubbedHttp(responses));
}
// ...
}
class StubbedHttp {
constructor(private readonly _response?: NulledHttpClientResponse) {
}
request() {
return new StubbedRequest(this._response);
}
}
class StubbedRequest extends EventEmitter {
constructor(private readonly _response?: NulledHttpClientResponse) {
super();
}
end() {
setImmediate(() => {
this.emit("response", new StubbedResponse(this._response));
});
}
}
class StubbedResponse extends EventEmitter implements NodeHttpResponse {
statusCode: number;
headers: HttpHeaders;
constructor(response: NulledHttpClientResponse = DEFAULT_NULLED_RESPONSE) {
super();
this.statusCode = response.status ?? 42;
this.headers = response.headers ?? {};
setImmediate(() => {
this.emit("data", response.body);
this.emit("end");
});
}
}