Challenge #9: The Last Request

The spy server still has a lot of hardcoded logging in it. In this challenge, you’ll convert that code into an assertion, eliminating the need to manually check your test output.

Instructions

1. Assert on the last request:

  1. Add a spyServer.lastRequest property that exposes an object with the following data:
    {
    	method,   // the most recent request’s HTTP method
    	path,     // the most recent request’s URL
    	headers,  // the most recent request’s headers
    	body,     // the most recent request’s body
    }
  2. Convert the request logging to an assertion.
  3. Add a spyServer.reset() method that resets spyServer.lastRequest to null.
  4. Add a beforeEach() function that calls spyServer.reset() before each test runs.

2. The test should pass and you should no longer have any logs in your test output.

Remember to commit your changes when you’re done.

API Documentation

beforeEach(() => { /* code */ });

Tell the test runner to run the code before each test in this describe() block.

JavaScript Primers

Hints

1
The request data you want to expose needs to be in variables.
Specifically, the method, path, headers, and body all need variables.
body is already in a variable. Create variables for method, path, and headers. Your editor might have an “Introduce Variable” refactoring that does this for you automatically.
async startAsync() {
	this._server = http.createServer();

	await new Promise((resolve, reject) => {
		this._server.listen(PORT);
		this._server.on("listening", () => resolve());
	});

	this._server.on("request", (serverRequest, serverResponse) => {
		console.log("SERVER RECEIVING REQUEST");

		let body = "";
		serverRequest.on("data", (chunk) => {
			body += chunk;
		});

		serverRequest.on("end", () => {
			const method = serverRequest.method;
			const path = serverRequest.url;
			const headers = serverRequest.headers;

			console.log("SERVER RECEIVED ENTIRE REQUEST");
			console.log("SERVER RECEIVED METHOD:", method);
			console.log("SERVER RECEIVED PATH:", path);
			console.log("SERVER RECEIVED HEADERS:", headers);
			console.log("SERVER RECEIVED BODY:", body);

			serverResponse.statusCode = 999;
			serverResponse.setHeader("myResponseHeader", "myResponseValue");
			serverResponse.end("my response body");

			console.log("SERVER SENT RESPONSE");
		});
	});
}
2
You’re ready to provide the spyServer.lastRequest property.
All you have to do is set this.lastRequest.
async startAsync() {
	this._server = http.createServer();

	await new Promise((resolve, reject) => {
		this._server.listen(PORT);
		this._server.on("listening", () => resolve());
	});

	this._server.on("request", (serverRequest, serverResponse) => {
		console.log("SERVER RECEIVING REQUEST");

		let body = "";
		serverRequest.on("data", (chunk) => {
			body += chunk;
		});

		serverRequest.on("end", () => {
			const method = serverRequest.method;
			const path = serverRequest.url;
			const headers = serverRequest.headers;

			console.log("SERVER RECEIVED ENTIRE REQUEST");
			console.log("SERVER RECEIVED METHOD:", method);
			console.log("SERVER RECEIVED PATH:", path);
			console.log("SERVER RECEIVED HEADERS:", headers);
			console.log("SERVER RECEIVED BODY:", body);

			this.lastRequest = {
				method,
				path,
				headers,
				body,
			};

			serverResponse.statusCode = 999;
			serverResponse.setHeader("myResponseHeader", "myResponseValue");
			serverResponse.end("my response body");

			console.log("SERVER SENT RESPONSE");
		});
	});
}

TypeScript needs the type of lastRequest to be declared. That’s easiest if you introduce an interface:

interface SpyServerRequest {
	method: string,
	path: string,
	headers: NodeIncomingHttpHeaders,
	body: string,
}

class SpyServer {

	_server?: http.Server;
	lastRequest?: SpyServerRequest;

	async startAsync() {
		await new Promise<void>((resolve, reject) => {
			this._server = http.createServer();

			this._server.listen(PORT);
			this._server.on("listening", () => resolve());

			this._server.on("request", (serverRequest, serverResponse) => {
				console.log("SERVER RECEIVING REQUEST");

				let body = "";
				serverRequest.on("data", (chunk) => {
					body += chunk;
				});

				serverRequest.on("end", () => {
					const method = serverRequest.method;
					const path = serverRequest.url;
					const headers = serverRequest.headers;

					console.log("SERVER RECEIVED ENTIRE REQUEST");
					console.log("SERVER RECEIVED METHOD:", method);
					console.log("SERVER RECEIVED PATH:", path);
					console.log("SERVER RECEIVED HEADERS:", headers);
					console.log("SERVER RECEIVED BODY:", body);

					this.lastRequest = {
						method,
						path,
						headers,
						body,
					} as SpyServerRequest;

					serverResponse.statusCode = 999;
					serverResponse.setHeader("myResponseHeader", "myResponseValue");
					serverResponse.end("my response body");

					console.log("SERVER SENT RESPONSE");
				});
			});
		});
	}

	// ...
}
3
Now you can introduce the assertion.
You can use assert.deepEqual() for that.
it("performs request", async () => {
	console.log("CLIENT SENDING REQUEST");
	const clientRequest = http.request({
		host: HOST,
		port: PORT,
		method: "POST",
		path: "/my/path",
		headers: {
			myRequestHeader: "myRequestValue",
		},
	});
	clientRequest.end("my request body");

	const response = await new Promise((resolve, reject) => {
		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode,
					headers: clientResponse.headers,
					body,
				});
			});
		});
	});
	console.log("EXCHANGE COMPLETE");

	assert.deepEqual(spyServer.lastRequest, {
		method: "POST",
		path: "/my/path",
		headers: {
			myrequestheader: "myRequestValue",
			host: `${HOST}:${PORT}`,
			connection: "close",
			"content-length": "15",
		},
		body: "my request body",
	});

	delete response.headers.date;
	assert.deepEqual(response, {
		status: 999,
		headers: {
			myresponseheader: "myResponseValue",
			connection: "close",
			"content-length": "16",
		},
		body: "my response body",
	});
});

(The assertion uses string interpolation.)

4
The test should pass. You’re ready to add spyServer.reset() and the beforeEach() block.
You might as well call reset() in the constructor, just to be clean about it.
describe.only("HTTP Client", () => {

	let spyServer;

	before(async () => {
		spyServer = new SpyServer();
		await spyServer.startAsync();
	});

	beforeEach(() => {
		spyServer.reset();
	});

	after(async () => {
		await spyServer.stopAsync();
	});

	// ...
});


class SpyServer {

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	// ...
}

TypeScript needs to be told that lastRequest can be null, and never undefined.

class SpyServer {

	_server?: http.Server;
	lastRequest!: SpyServerRequest | null;

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	// ...
}
5
It’s time to clean up.
Delete the remaining logs and inline the lastRequest variables.
it("performs request", async () => {
	// remove log

	const clientRequest = http.request({
		host: HOST,
		port: PORT,
		method: "POST",
		path: "/my/path",
		headers: {
			myRequestHeader: "myRequestValue",
		},
	});
	clientRequest.end("my request body");

	const response = await new Promise((resolve, reject) => {  // add <HttpClientResponse> in TypeScript
		clientRequest.on("response", (clientResponse) => {
			let body = "";
			clientResponse.on("data", (chunk) => {
				body += chunk;
			});

			clientResponse.on("end", () => {
				resolve({
					status: clientResponse.statusCode,  // add 'as number' in TypeScript
					headers: clientResponse.headers,    // add 'as HttpHeaders' in TypeScript
					body,
				});
			});
		});
	});
	// remove log

	assert.deepEqual(spyServer.lastRequest, {
		method: "POST",
		path: "/my/path",
		headers: {
			myrequestheader: "myRequestValue",
			host: `${HOST}:${PORT}`,
			connection: "close",
			"content-length": "15",
		},
		body: "my request body",
	});

	delete response.headers.date;
	assert.deepEqual(response, {
		status: 999,
		headers: {
			myresponseheader: "myResponseValue",
			connection: "close",
			"content-length": "16",
		},
		body: "my response body",
	});
});

// ...

async startAsync() {
	this._server = http.createServer();

	await new Promise((resolve, reject) => {
		this._server.listen(PORT);
		this._server.on("listening", () => resolve());
	});

	this._server.on("request", (serverRequest, serverResponse) => {
		// remove log

		let body = "";
		serverRequest.on("data", (chunk) => {
			body += chunk;
		});

		serverRequest.on("end", () => {
			// remove log

			this.lastRequest = {
				method: serverRequest.method,
				path: serverRequest.url,
				headers: serverRequest.headers,
				body,
			};  // add 'as SpyServerRequest' in TypeScript

			serverResponse.statusCode = 999;
			serverResponse.setHeader("myResponseHeader", "myResponseValue");
			serverResponse.end("my response body");

			// remove log
		});
	});
}

Complete Solution

Test code (JavaScript):
describe.only("HTTP Client", () => {

	let spyServer;

	before(async () => {
		spyServer = new SpyServer();
		await spyServer.startAsync();
	});

	beforeEach(() => {
		spyServer.reset();
	});

	after(async () => {
		await spyServer.stopAsync();
	});

	describe("happy path", () => {

		it("performs request", async () => {
			const clientRequest = http.request({
				host: HOST,
				port: PORT,
				method: "POST",
				path: "/my/path",
				headers: {
					myRequestHeader: "myRequestValue",
				},
			});
			clientRequest.end("my request body");

			const response = await new Promise((resolve, reject) => {
				clientRequest.on("response", (clientResponse) => {
					let body = "";
					clientResponse.on("data", (chunk) => {
						body += chunk;
					});

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

			assert.deepEqual(spyServer.lastRequest, {
				method: "POST",
				path: "/my/path",
				headers: {
					myrequestheader: "myRequestValue",
					host: `${HOST}:${PORT}`,
					connection: "close",
					"content-length": "15",
				},
				body: "my request body",
			});

			delete response.headers.date;
			assert.deepEqual(response, {
				status: 999,
				headers: {
					myresponseheader: "myResponseValue",
					connection: "close",
					"content-length": "16",
				},
				body: "my response body",
			});
		});

	// ...
});


class SpyServer {

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	async startAsync() {
		this._server = http.createServer();

		await new Promise((resolve, reject) => {
			this._server.listen(PORT);
			this._server.on("listening", () => resolve());
		});

		this._server.on("request", (serverRequest, serverResponse) => {
			let body = "";
			serverRequest.on("data", (chunk) => {
				body += chunk;
			});

			serverRequest.on("end", () => {
				this.lastRequest = {
					method: serverRequest.method,
					path: serverRequest.url,
					headers: serverRequest.headers,
					body,
				};

				serverResponse.statusCode = 999;
				serverResponse.setHeader("myResponseHeader", "myResponseValue");
				serverResponse.end("my response body");
			});
		});
	}

	async stopAsync() {
		await new Promise((resolve, reject) => {
			this._server.close();
			this._server.on("close", () => resolve());
		});
	}

}
Test code (TypeScript):
describe.only("HTTP Client", () => {

	let spyServer: SpyServer;

	before(async () => {
		spyServer = new SpyServer();
		await spyServer.startAsync();
	});

	beforeEach(() => {
		spyServer.reset();
	});

	after(async () => {
		await spyServer.stopAsync();
	});

	describe("happy path", () => {

		it("performs request", async () => {
			const clientRequest = http.request({
				host: HOST,
				port: PORT,
				method: "POST",
				path: "/my/path",
				headers: {
					myRequestHeader: "myRequestValue",
				},
			});
			clientRequest.end("my request body");

			const response = await new Promise<HttpClientResponse>((resolve, reject) => {
				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,
						});
					});
				});
			});

			assert.deepEqual(spyServer.lastRequest, {
				method: "POST",
				path: "/my/path",
				headers: {
					myrequestheader: "myRequestValue",
					host: `${HOST}:${PORT}`,
					connection: "close",
					"content-length": "15",
				},
				body: "my request body",
			});

			delete response.headers.date;
			assert.deepEqual(response, {
				status: 999,
				headers: {
					myresponseheader: "myResponseValue",
					connection: "close",
					"content-length": "16",
				},
				body: "my response body",
			});
		});

	// ...
});


interface SpyServerRequest {
	method: string,
	path: string,
	headers: NodeIncomingHttpHeaders,
	body: string,
}

class SpyServer {

	_server?: http.Server;
	lastRequest!: SpyServerRequest | null;

	constructor() {
		this.reset();
	}

	reset() {
		this.lastRequest = null;
	}

	async startAsync() {
		await new Promise<void>((resolve, reject) => {
			this._server = http.createServer();

			this._server.listen(PORT);
			this._server.on("listening", () => resolve());

			this._server.on("request", (serverRequest, serverResponse) => {
				let body = "";
				serverRequest.on("data", (chunk) => {
					body += chunk;
				});

				serverRequest.on("end", () => {
					this.lastRequest = {
						method: serverRequest.method,
						path: serverRequest.url,
						headers: serverRequest.headers,
						body,
					} as SpyServerRequest;

					serverResponse.statusCode = 999;
					serverResponse.setHeader("myResponseHeader", "myResponseValue");
					serverResponse.end("my response body");
				});
			});
		});
	}

	async stopAsync() {
		await new Promise<void>((resolve, reject) => {
			if (this._server === undefined) return reject(new Error("SpyServer has not been started"));

			this._server.close();
			this._server.on("close", () => resolve());
		});
	}

}

No production code yet.

Next challenge

Return to module overview