Challenge #1: Injecting the Stub
Low-level Nullables are implemented with an Embedded Stub that’s entirely encapsulated, and thus invisible to callers. That’s what makes it an embedded stub: it’s an implementation detail that callers should neither know nor care about.
In this challenge, you’ll start introducing an embedded stub into the HttpClient
class. That will take place in three parts: First, you’ll create a test that checks whether the httpClient
is sending requests. Second, you’ll modify the create()
and createNull()
factories to inject real and stubbed versions of the http
library. Finally, you’ll confirm that createNull()
uses the stub. In the next challenge, you’ll make the stub work.
Instructions
1. In the "doesn't talk to network"
test:
- Call
httpClient.requestAsync()
with a normal (non-Nulled) instance ofHttpClient
. - Assert that the
SpyServer
doesn’t see a request. The test should fail. - Comment out the test.
2. In the production code:
- Modify
create()
to inject thehttp
library. - Modify the rest of the production code to use the injected library (rather than the imported variable).
- Modify
createNull()
to inject a do-nothing class namedStubbedHttp
. - Make the stub log
"STUB CALLED"
whenHttpClient.requestAsync()
is called.
3. In the "doesn't talk to network"
test:
- Uncomment the test.
- Change it use a Nulled instance of
HttpClient
. - The test should still fail, but you should see
"STUB CALLED"
in the test output.
Remember to commit your changes when you’re done.
API Documentation
await requestAsync({ client });
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()
const request = spyServer.lastRequest;
The tests create a localhost
HTTP server and make it available via the spyServer
variable. It records the HTTP requests made by requestAsync()
. The lastRequest
property exposes the most recent request.
- returns request
({ method, path, headers, body } | null)
- the most recent request, ornull
if no requests have been made; resets before every test
const httpClient = HttpClient.create();
Create a normal HttpClient
.
- returns httpClient
(HttpClient)
- the HTTP client
const httpClient = HttpClient.createNull();
Create a Nulled HttpClient
.
- returns httpClient
(HttpClient)
- the HTTP client
console.log(data);
Convert data
to a string and write it to stdout
, followed by a newline (\n
).
- data
(any)
- the data to write
assert.equal(actual, expected)
Assert that two variables are strictly equal (===
). This is usually used for primitive types. To compare objects and arrays, including their contents, use assert.deepEqual()
instead.
- actual
(any)
- the actual value - expected
(any)
- the expected value
JavaScript Primers
Hints
In the "doesn't talk to network"
test:
1
The test needs to call httpClient.requestAsync()
.
You can use the tests’ requestAsync()
helper to do that. Remember to await
it.
it("doesn't talk to network", async () => {
await requestAsync();
});
2
The test needs to assert that the spyServer
didn’t see a request.
You can use spyServer.lastRequest
for that.
It’s null
if there hasn’t been a request.
You can use assert.equal()
for the assertion.
it("doesn't talk to network", async () => {
await requestAsync();
assert.equal(spyServer.lastRequest, null);
});
3
You’re ready to run the test.
It should fail, saying it expected null
.
This is because the test is making a real HTTP request.
4
Set the test aside for now.
Comment it out.
it("doesn't talk to network", async () => {
// await requestAsync();
// assert.equal(spyServer.lastRequest, null);
});
In the production code:
5
You need to inject the http
library.
You can do that in HttpClient.create()
.
Pass it as a parameter and store it in an instance variable.
static create() {
return new HttpClient(http);
}
// ...
constructor(http) {
this._http = http;
}
TypeScript requires a type declaration for the http
parameter. Use any
as a temporary placeholder. (You’ll fix it in the next challenge.)
constructor(private readonly _http: any) {
}
6
The production code needs to use the injected library.
That’s a matter of using this._http
instead of http
directly.
There’s only one line of code that needs to be changed.
It’s the call to http.request()
.
#sendRequest(host, port, method, path, headers, body) {
const clientRequest = this._http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
(The hash at the beginning of #sendRequest()
means it’s a private method.)
7
You’ll need a StubbedHttp
class to accomplish your next task.
Put it at the bottom of the file. It doesn’t need to do anything.
class StubbedHttp {
}
8
The createNull()
factory needs to inject the stub.
Just instantiate the stub and pass it in.
static createNull(responses) {
return new HttpClient(new StubbedHttp());
}
9
StubbedHttp
needs to log when HttpClient.requestAsync()
is called.
You can use console.log()
to do the logging.
You need to put the logging in a StubbedHttp
method that HttpClient.requestAsync()
will call.
The StubbedHttp
instance will be in this._http
.
What methods does HttpClient.requestAsync()
call on this._http
? Trace through the code.
It’s calling this._http.request()
in #sendRequest()
.
Add the logging code to StubbedHttp.request()
.
class StubbedHttp {
request() {
console.log("STUB CALLED");
}
}
In the "doesn't talk to network"
test:
10
Uncomment the test.
it("doesn't talk to network", async () => {
await requestAsync();
assert.equal(spyServer.lastRequest, null);
});
11
You can run the test.
It should be failing in the same way as before, expecting null
.
That’s because it’s not using the new code you wrote.
It needs to use a HttpClient.createNull()
.
12
The test needs to use HttpClient.createNull()
.
The first step is to instantiate it.
it("doesn't talk to network", async () => {
const client = HttpClient.createNull();
await requestAsync();
assert.equal(spyServer.lastRequest, null);
});
13
The request needs to use the Nulled instance of HttpClient
.
You can pass it as a parameter to requestAsync()
. (Object shorthand is useful here.)
it("doesn't talk to network", async () => {
const client = HttpClient.createNull();
await requestAsync({ client });
assert.equal(spyServer.lastRequest, null);
});
14
You’re ready to run the test.
It should fail, complaining about an undefined
behavior.
That’s expected at this point. You’re supposed to be looking at the test output.
It should say STUB CALLED
.
That’s because httpClient.requestAsync()
is calling this._http.request()
, which is running StubbedHttp.request()
, which is logging STUB CALLED
.
Complete Solution
Test code:
it("doesn't talk to network", async () => {
const client = HttpClient.createNull();
await requestAsync({ client });
assert.equal(spyServer.lastRequest, null);
});
Production code (JavaScript):
export class HttpClient {
static create() {
return new HttpClient(http);
}
static createNull(responses) {
return new HttpClient(new StubbedHttp());
}
constructor(http) {
this._http = http;
}
trackRequests() {
throw new Error("not implemented");
}
async requestAsync({ host, port, method, path, headers = {}, body = ""}) {
if (method.toLowerCase() === "get" && body !== "") {
throw new Error("Don't include body with GET requests; Node won't send it");
}
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(host, port, method, path, headers, body) {
const clientRequest = this._http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest) {
return await new Promise((resolve, reject) => {
clientRequest.on("error", (err) => reject(err));
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode,
headers: clientResponse.headers,
body,
});
});
});
});
}
}
class StubbedHttp {
request() {
console.log("STUB CALLED");
}
}
Production code (TypeScript):
export class HttpClient {
static create(): HttpClient {
return new HttpClient(http);
}
static createNull(
responses?: NulledHttpClientResponse | NulledHttpClientResponses
): HttpClient {
return new HttpClient(new StubbedHttp());
}
constructor(private readonly _http: any) {
}
trackRequests(): OutputTracker<HttpClientRequestParameters> {
throw new Error("not implemented");
}
async requestAsync({
host,
port,
method,
path,
headers = {},
body = ""
}: HttpClientRequestParameters): Promise<HttpClientResponse> {
if (method.toLowerCase() === "get" && body !== "") {
throw new Error("Don't include body with GET requests; Node won't send it");
}
const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
return await this.#handleResponseAsync(clientRequest);
}
#sendRequest(
host: string,
port: number,
method: string,
path: string,
headers: HttpHeaders,
body: string
): http.ClientRequest {
const clientRequest = this._http.request({
host: host,
port: port,
method: method,
path: path,
headers: headers,
});
clientRequest.end(body);
return clientRequest;
}
async #handleResponseAsync(clientRequest: http.ClientRequest): Promise<HttpClientResponse> {
return await new Promise((resolve, reject) => {
clientRequest.on("error", (err) => reject(err));
clientRequest.on("response", (clientResponse) => {
let body = "";
clientResponse.on("data", (chunk) => {
body += chunk;
});
clientResponse.on("end", () => {
resolve({
status: clientResponse.statusCode as number,
headers: clientResponse.headers as HttpHeaders,
body,
});
});
});
});
}
/* Placeholder to satisfy TypeScript compiler */
request(options: any): { responsePromise: Promise<HttpClientResponse>, cancelFn: (message: string) => boolean } {
throw new Error("not implemented");
}
}
class StubbedHttp {
request() {
console.log("STUB CALLED");
}
}