Challenge #2: Stubbing Behavior

In the last challenge, you made createNull() inject an instance of the StubbedHttp class. StubbedHttp isn’t implemented yet, so the test is failing. In this challenge, you’ll implement StubbedHttp.

Instructions

1. Analyze the code:

  1. Mark the "performs request" test with it.only() so it’s the only test that is run.
  2. Use console.log() to instrument the code. The "performs request" test should write the following output to the console:
    BEFORE SEND_REQUEST
    BEFORE HTTP.REQUEST
    BEFORE CLIENT_REQUEST.END
    BEFORE HANDLE_RESPONSE
    START HANDLE_RESPONSE_ASYNC
    INSIDE HANDLE_RESPONSE_ASYNC PROMISE
    INSIDE RESPONSE EVENT
    INSIDE DATA EVENT
    INSIDE END EVENT
  3. Trace through the code to understand how this._http and its return values are used.

2. Implement the stub:

  1. Remove the .only() from the "performs request" test and put it on the "doesn't talk to network" test instead.
  2. Use the console logs to understand what’s missing from the StubbedHttp class.
  3. Iteratively modify the StubbedHttp class, using the logs as a guide, until the test passes. Implement as little code as possible. (The Creating Event Emitters primer will be helpful here.)
  4. Remove the instrumentation, including the "STUB CALLED" log.

Remember to commit your changes when you’re done.

Understanding HttpClient

These are the methods used by HttpClient:

const clientRequest = this._http.request({ host, port, method, path, headers });

Start making an HTTP request. Note that the request isn’t complete until request.end() is called.

  • host (string) - the server host
  • port (number) - the server port
  • method (string) - the HTTP method
  • path (string) - the HTTP path
  • optional headers (object) - the HTTP headers; defaults to none
  • returns clientRequest (http.ClientRequest) - the request
clientRequest.end(body);

Complete the HTTP request by sending the request body (if any). This function returns immediately, before all the data has been sent.

Important! body will be ignored if the HTTP method can’t have a body. For example, GET requests can’t have a body, but POST requests can.

  • optional body (string) - the request body
clientRequest.on("error", fn);

Run fn when the client is unable to make the request, such as when the connection is refused.

  • fn ((err) => void) - the function to run
  • err (Error) - the error that occurred
clientRequest.on("response", fn);

Run fn when the client receives a response from the server.

  • fn ((clientResponse) => void) - the function to run
  • clientResponse (http.IncomingMessage) - the client’s view of the server’s response
clientResponse.on("data", fn);

Run fn when the client receives body data from the server. Note that the server can send multiple pieces of data, so fn can be called multiple times. Accumulate it into a string like this:

This is a replacement for clientResponse.resume()

let data = "";
clientResponse.on("data", (chunk) => {
	data += chunk;
}
  • fn ((chunk) => void) - the function to run
  • chunk (Buffer) - the data
clientResponse.on("end", fn);

Run fn when the client sees the end of the server‘s response. Important: for this event to occur, you must consume the response’s data. You can use clientResponse.resume() to do so.

  • fn ((clientResponse) => void) - the function to run
  • clientResponse (http.IncomingMessage) - the client’s view of the server’s response
const status = clientResponse.statusCode;

The response’s status code.

  • returns status (number) - the status code
const headers = clientResponse.headers;

The response’s headers. Header names and values are mapped to object names and values.

  • returns headers (object) - the headers

API Documentation

it.only(async () => { ... });

Tell the test runner to only run tests that use it.only(). This is handy for debugging and analysis: you can add console.log() statements to the production code and then mark a single test with it.only(), ensuring that the logging only happens once.

class MyClass extends EventEmitter { ... }

Extend EventEmitter to add support for the on() and emit() event handling methods to a class. This is useful when creating stubs of classes that emit events, such as clientRequest.

Important! If your code has a constructor, the constructor must call super() as its first line.

this.emit(eventName, eventValue);

Emits eventName, which will run fn() on any code that has previously run on(eventName, fn) on the same object.

Best used in conjunction with setImmediate() to emit events asynchronously.

  • eventName (string) - the function to run
setImmediate(fn);

Run fn() later, after the current code is complete or awaiting a promise that has yet to resolve. Practically speaking, this function can be used to emulate events that occur asynchronously, such as clientRequest.on("response", ...). Use it like this:

setImmediate(() => {
	this.emit("response", myResponse);
}
  • fn (function) - the function to run

TypeScript Types

response.on("data", (chunk: Buffer) => ...)

The chunk provided to the data event is a Buffer.

JavaScript Primers

Hints

Analyzing execution:

1
Start by analyzing the HttpClient code. How does the flow of execution work? (You might need to review the Using Promises, Creating Promises, and Using Events primers.)
You can understand it better adding logging statements and marking a test it.only().
Try marking the "performs request" test with it.only().

Test code:

it.only("performs request", async () => {
	// ...
});

Production code:

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");
	}

	console.log("BEFORE SEND_REQUEST");
	const clientRequest = this.#sendRequest(host, port, method, path, headers, body);
	console.log("BEFORE HANDLE_RESPONSE");
	return await this.#handleResponseAsync(clientRequest);
}

#sendRequest(host, port, method, path, headers, body) {
	console.log("BEFORE HTTP.REQUEST");
	const clientRequest = this._http.request({
		host: host,
		port: port,
		method: method,
		path: path,
		headers: headers,
	});
	console.log("BEFORE CLIENT_REQUEST.END");
	clientRequest.end(body);

	return clientRequest;
}

async #handleResponseAsync(clientRequest) {
	console.log("START HANDLE_RESPONSE_ASYNC");
	return await new Promise((resolve, reject) => {
		console.log("INSIDE HANDLE_RESPONSE_ASYNC PROMISE");
		clientRequest.on("error", (err) => reject(err));

		clientRequest.on("response", (clientResponse) => {
			console.log("INSIDE RESPONSE EVENT");
			let body = "";
			clientResponse.on("data", (chunk) => {
				console.log("INSIDE DATA EVENT");
				body += chunk;
			});

			clientResponse.on("end", () => {
				console.log("INSIDE END EVENT");
				resolve({
					status: clientResponse.statusCode,  // add 'as number' for TypeScript
					headers: clientResponse.headers,    // add 'as HttpHeaders' for TypeScript
					body,
				});
			});
		});
	});
}

The test output looks like this:

BEFORE SEND_REQUEST
BEFORE HTTP.REQUEST
BEFORE CLIENT_REQUEST.END
BEFORE HANDLE_RESPONSE
START HANDLE_RESPONSE_ASYNC
INSIDE HANDLE_RESPONSE_ASYNC PROMISE
INSIDE RESPONSE EVENT
INSIDE DATA EVENT
INSIDE END EVENT
2
Now that you know what the flow looks like when it’s working normally, look at what it looks like when using HttpClient.createNull().
Remove the it.only() from the "performs request" test and put it on the "doesn't talk to network" test instead.

Test code:

it("performs request", async () => {
	// ...
});

// ...

it.only("doesn't talk to network", async () => {
	// ...
});

Test output:

BEFORE SEND_REQUEST
BEFORE HTTP.REQUEST
STUB CALLED
BEFORE CLIENT_REQUEST.END

BEFORE HANDLE_RESPONSE:

3
The code isn’t logging BEFORE HANDLE_RESPONSE. Why not?
Look at the stack trace. Why is the test failing?
It’s complaining that end() is being called on undefined.
It’s crashing on the line that reads clientRequest.end().

The code isn’t logging BEFORE HANDLE_RESPONSE because clientRequest is undefined, which is causing the code to crash before #sendRequest returns.

4
Why is clientRequest undefined?
Where does it come from?
It’s defined in the same function.
It’s returned by this._http.request().

clientRequest is undefined because this._http.request() isn’t returning a value.

5
this._http.request() isn’t returning a value. Why not?
It’s only a problem in the nulled instance test.
That test is calling HttpClient.createNull().
HttpClient.createNull() is setting this._http to an instance of StubbedHttp.

this._http.request() isn’t returning a value because StubbedHttp.request() isn’t returning a value.

6
What should StubbedHttp.request() return?
What does the real http module return?
It returns clientRequest, which is an http.ClientRequest.
Using real-world infrastructure classes like http.ClientRequest is always messy. What could you do instead?
You can make a stub.

StubbedHttp.request() should return a StubbedRequest.

7
You need a StubbedRequest class.
It should be returned by StubbedHttp.request().
You can remove the "STUB CALLED" logging.
class StubbedHttp {

	request() {
		return new StubbedRequest();
	}

}

class StubbedRequest {
}
8
The error message should have changed to clientRequest.end is not a function.
That means clientRequest is no longer undefined. Returning StubbedRequest worked.
But clientRequest.end() doesn’t exist.

That’s because you haven’t implemented StubbedRequest.end().

9
You need to implement StubbedRequest.end(). What should it do?
What does the real clientRequest.end() do?
It sends an HTTP request to the server.
But the Nulled HttpClient doesn’t send requests.

StubbedRequest.end() shouldn’t do anything.

10
Implement StubbedRequest.end().
It doesn’t need to do anything.
class StubbedRequest {

	end() {
	}

}

INSIDE RESPONSE EVENT:

11
The test has made more progress, but now it isn’t logging INSIDE RESPONSE_EVENT. Why not?
Look at the stack trace. Why is the test failing?
It’s complaining that clientRequest.on is not a function.

That’s because you haven’t implemented StubbedRequest.on().

12
StubbedRequest needs to implement on(). What’s the best way to do that?
on() is part of Node.js’s standard event handling behavior.
It’s not infrastructure code, so it’s okay to use it in a stub.
You can add event handling behavior to a class by by extending EventEmitter.
class StubbedRequest extends EventEmitter {

	end() {
	}

}
13
Now the test is timing out. Why?
Which log message is supposed to be written next?
It’s INSIDE RESPONSE EVENT.
That message isn’t being written because the response event isn’t firing.

StubbedRequest needs to emit the response event.

14
StubbedRequest needs to emit the response event. Where should it do it?
When does the event normally fire?
It normally fires after the server receives the request.
When does the server receive the request?
After clientRequest.end() executes.

StubbedRequest should emit the response event inside clientRequest.end().

15
Make StubbedRequest emit the response event.
You can use this.emit() to do that.
It should simulate the real code’s asynchronous behavior, and emit the event after clientRequest.on() is called.
You can use setImmediate() to delay the call to this.emit(). (It uses an arrow function.)
class StubbedRequest extends EventEmitter {

	end() {
		setImmediate(() => {
			this.emit("response");
		});
	}

}

INSIDE DATA EVENT:

16
Now the test isn’t logging INSIDE DATA EVENT. Why not?
Look at the stack trace. Why is the test failing?
It’s complaining that on() is being called on undefined.
It’s crashing on the line that calls clientResponse.on("data", ...).

The code isn’t logging INSIDE_DATA_EVENT because clientResponse is undefined.

17
Why is clientResponse undefined?
Where does it come from?
It’s received by the clientRequest.on("request", clientResponse) event handler.
That event is being emitted in StubbedRequest.end().

clientResponse is undefined because StubbedRequest.end() isn’t emitting a value with the "response" event.

18
What value should StubbedRequest.end() emit?
What does the real clientRequest emit?
It emits clientResponse, which is an http.IncomingMessage.
Using real-world infrastructure classes like http.IncomingMessage is always messy. What could you do instead?
You can make a stub.

StubbedRequest.end() should emit a StubbedResponse.

19
You need a StubbedResponse class.
It should be emitted by StubbedRequest.end().
class StubbedRequest extends EventEmitter {

	end() {
		setImmediate(() => {
			this.emit("response", new StubbedResponse());
		});
	}

}

class StubbedResponse {
}
20
Now the test is complaining that clientResponse.on is not a function. Why?
What is clientResponse?
It’s a StubbedResponse.

You haven’t implemented StubbedResponse.on().

21
What’s the best way to implement StubbedResponse.on()?
on() is part of Node.js’s standard event handling behavior.
It’s not infrastructure code, so it’s okay to use it in a stub.
You can add event handling behavior to a class by by extending EventEmitter.
class StubbedResponse extends EventEmitter {
}
22
Now the test is timing out. Why?
Which log message is supposed to be written next?
It’s INSIDE DATA EVENT.
That message isn’t being written because the data event isn’t firing.

StubbedResponse needs to emit the data event.

23
StubbedResponse needs to emit the data event. Where should it do it?
When does the event normally fire?
It normally fires immediately after the response event occurs.

StubbedResponse should emit the data event when it’s constructed.

24
Make StubbedResponse emit the data event.
You can use this.emit() to do that.
It should simulate the real code’s asynchronous behavior, and emit the event after clientResponse.on("data", ...) is called.
You can use setImmediate() to delay the call to this.emit().
class StubbedResponse extends EventEmitter {

	constructor() {
		super();
		setImmediate(() => {
			this.emit("data");
		});
	}

}

INSIDE END EVENT:

25
Now the test is timing out. Why?
Which log message is supposed to be written next?
It’s INSIDE END EVENT.
That message isn’t being written because the end event isn’t firing.

StubbedResponse needs to emit the end event.

26
StubbedResponse needs to emit the end event. Where should it do it?
When does the event normally fire?
It normally fires after the last data event.

StubbedResponse should emit the end event after the data event.

27
Make StubbedResponse emit the end event.
You can use this.emit() to do that.
It should simulate the real code’s asynchronous behavior, and emit the event after the data event.
You can use your existing setImmediate() block.
class StubbedResponse extends EventEmitter {

	constructor() {
		super();
		setImmediate(() => {
			this.emit("data");
			this.emit("end");
		});
	}

}
28
The test should pass. It’s time to clean up.
Remove the logs.

Test code:

it("doesn't talk to network", async () => {
	// ...
});

Production code:

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,  // add 'as number' for TypeScript
					headers: clientResponse.headers,    // add 'as HttpHeaders' for TypeScript
					body,
				});
			});
		});
	});
}

TypeScript types (skip these steps if you’re using JavaScript):

29
TypeScript needs a type for this._http. Call it NodeHttp.
Don’t use http’s full interface; instead, just declare the minimum interface that your code actually uses.
You’ve just gone through the effort of figuring out what your code actually uses.
It’s the same interface as StubbedHttp.
The only difference is that you’ll need to declare all the parameters #sendRequest() uses.
interface NodeHttp {
	request: ({ host, port, method, path, headers }: {
		host: string,
		port: number,
		method: string,
		path: string,
		headers: HttpHeaders,
	}) => NodeHttpRequest,
}
30
TypeScript needs a type for the return value of NodeHttp.request(). Call it NodeHttpRequest.
Don’t use http.ClientRequest’s full interface; instead, just declare the minimum interface that your code actually uses.
It’s the same as StubbedRequest.
interface NodeHttpRequest extends EventEmitter {
	end: (body?: string) => void,
}
31
Now you can use the new types in your code.
Start by replacing the any in the constructor with NodeHttp.
constructor(private readonly _http: NodeHttp) {
}
32
After adding NodeHttp to the constructor, the compiler will complain about missing properties.
It’s complaining about the return type of #sendRequest().
The declared return type for #sendRequest() is too broad.
Replace it with NodeHttpRequest.
#sendRequest(
	host: string,
	port: number,
	method: string,
	path: string,
	headers: HttpHeaders,
	body: string
): NodeHttpRequest {
	const clientRequest = this._http.request({
		host: host,
		port: port,
		method: method,
		path: path,
		headers: headers,
	});
	clientRequest.end(body);

	return clientRequest;
}
33
After adding NodeHttpRequest to #sendRequest(), the compiler will complain about missing properties again.
It’s the same error, just a different line.
One of the declared function parameters for #handleResponseAsync() is too broad.
Replace http.ClientRequest with NodeHttpRequest.
async #handleResponseAsync(clientRequest: NodeHttpRequest): 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,
				});
			});
		});
	});
}
34
Now the compiler is complaining about the chunk parameter.
The correct type is Buffer.
async #handleResponseAsync(clientRequest: NodeHttpRequest): Promise<HttpClientResponse> {
	return await new Promise((resolve, reject) => {
		clientRequest.on("error", (err) => reject(err));

		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk: Buffer) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode as number,
					headers: clientResponse.headers as HttpHeaders,
					body,
				});
			});
		});
	});
}
35
The code compiles and passes tests, but there are still two implicit any types in the code. Where are they?
It’s because TypeScript considers event handlers’ values to have an any type.
Look for event handlers that take a parameter and don’t have a type declaration.

The clientRequest.on("error", ...) handler and the clientResponse.on("response", ...) handlers both have any types.

36
Declare a type for the clientRequest.on("error", ...) event handler.
It’s an Error.
async #handleResponseAsync(clientRequest: NodeHttpRequest): Promise<HttpClientResponse> {
	return await new Promise((resolve, reject) => {
		clientRequest.on("error", (err: Error) => reject(err));

		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk: Buffer) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode as number,
					headers: clientResponse.headers as HttpHeaders,
					body,
				});
			});
		});
	});
}
37
Declare a type for the clientResponse.on("response", ...) event handler.
You’ll need to create a new interface named NodeHttpResponse.
Create the minimum interface needed, based on what StubbedResponse and #handleResponseAsync() actually use.
Extend EventEmitter and declare statusCode and headers.
interface NodeHttpResponse extends EventEmitter {
	statusCode: number,
	headers: HttpHeaders,
}

// ...

async #handleResponseAsync(clientRequest: NodeHttpRequest): Promise<HttpClientResponse> {
	return await new Promise((resolve, reject) => {
		clientRequest.on("error", (err: Error) => reject(err));

		clientRequest.on("response", (clientResponse: NodeHttpResponse) => {
			let body = "";
			clientResponse.on("data", (chunk: Buffer) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode as number,
					headers: clientResponse.headers as HttpHeaders,
					body,
				});
			});
		});
	});
}
38
For completeness, one more set of types needs to be declared.
The stubs don’t implement the new interfaces.
Modify StubbedHttp, StubbedNodeRequest, and StubbedNodeResponse to implement NodeHttp, NodeHttpRequest, and NodeHttpResponse respectively.
You’ll have to add placeholders for status and headers to NodeHttpResponse. (They’ll be implemented in the next challenge.)
class StubbedHttp implements NodeHttp {

	request() {
		return new StubbedRequest();
	}

}

class StubbedRequest extends EventEmitter implements NodeHttpRequest {

	end() {
		setImmediate(() => {
			this.emit("response", new StubbedResponse());
		});
	}

}

class StubbedResponse extends EventEmitter implements NodeHttpResponse {

	statusCode = 42;
	headers = {};

	constructor() {
		super();

		setImmediate(() => {
			this.emit("data");
			this.emit("end");
		});
	}

}

Complete Solution

Production code (JavaScript):
class StubbedHttp {

	request() {
		return new StubbedRequest();
	}

}

class StubbedRequest extends EventEmitter {

	end() {
		setImmediate(() => {
			this.emit("response", new StubbedResponse());
		});
	}

}

class StubbedResponse extends EventEmitter {

	constructor() {
		super();
		setImmediate(() => {
			this.emit("data");
			this.emit("end");
		});
	}

}
Production code (TypeScript):
interface NodeHttp {
	request: ({ host, port, method, path, headers }: {
		host: string,
		port: number,
		method: string,
		path: string,
		headers: HttpHeaders,
	}) => NodeHttpRequest,
}

interface NodeHttpRequest extends EventEmitter {
	end: (body?: string) => void,
}

interface NodeHttpResponse extends EventEmitter {
	statusCode: number,
	headers: HttpHeaders,
}

export class HttpClient {
	// ...

	constructor(private readonly _http: NodeHttp) {
	}

	// ...

	#sendRequest(
		host: string,
		port: number,
		method: string,
		path: string,
		headers: HttpHeaders,
		body: string
	): NodeHttpRequest {
		const clientRequest = this._http.request({
			host: host,
			port: port,
			method: method,
			path: path,
			headers: headers,
		});
		clientRequest.end(body);

		return clientRequest;
	}

	async #handleResponseAsync(clientRequest: NodeHttpRequest): Promise<HttpClientResponse> {
		return await new Promise((resolve, reject) => {
			clientRequest.on("error", (err: Error) => reject(err));

			clientRequest.on("response", (clientResponse: NodeHttpResponse) => {
				let body = "";
				clientResponse.on("data", (chunk: Buffer) => {
					body += chunk;
				});

				clientResponse.on("end", () => {
					resolve({
						status: clientResponse.statusCode as number,
						headers: clientResponse.headers as HttpHeaders,
						body,
					});
				});
			});
		});
	}

	// ...
}


class StubbedHttp implements NodeHttp {

	request() {
		return new StubbedRequest();
	}

}

class StubbedRequest extends EventEmitter implements NodeHttpRequest {

	end() {
		setImmediate(() => {
			this.emit("response", new StubbedResponse());
		});
	}

}

class StubbedResponse extends EventEmitter implements NodeHttpResponse {

	statusCode = 42;  // placeholder
	headers = {};     // placeholder

	constructor() {
		super();

		setImmediate(() => {
			this.emit("data");
			this.emit("end");
		});
	}

}

Test code is unchanged.

Next challenge

Return to module overview