Challenge #8: The Spy Server

In this challenge, you’ll continue turning your proof-of-concept code into a real test by factoring out the test server. The eventual job of this server will be to report how the production code makes requests, like a spy, so it’s called a “Spy Server.”

Instructions

1. Factor out the Spy Server:

  1. Create a SpyServer class.
  2. Move the startup and request handling code into SpyServer.startAsync().
  3. Move the shutdown code into SpyServer.stopAsync().
  4. Add a before() function that starts the spy server before any of the tests run. Put it inside the top-level describe() block.
  5. Add an after() function that stops the spy server after all the tests run. Put it inside the top-level describe() block.
  6. Remove the startup and shutdown logging.

2. The test should pass. Your test output should be:

CLIENT SENDING REQUEST
SERVER RECEIVING REQUEST
SERVER RECEIVED ENTIRE REQUEST
SERVER RECEIVED METHOD: POST
SERVER RECEIVED PATH: /my/path
SERVER RECEIVED HEADERS: {
  myrequestheader: 'myRequestValue',
  host: 'localhost:5001',
  connection: 'close',
  'content-length': '15'
}
SERVER RECEIVED BODY: my request body
SERVER SENT RESPONSE
EXCHANGE COMPLETE

Remember to commit your changes when you’re done.

API Documentation

before(async () => { /* code */ });

Tell the test runner to run the code once, before any of the tests in this describe() block run.

after(async () => { /* code */ });

Tell the test runner to run the code once, after all the tests in this describe() block run. The code runs even if a test fails or throws an exception.

JavaScript Primers

Hints

1
Start by introducing the SpyServer class.
You can use the class keyword for that.
It doesn’t need to have any methods yet.
describe.only("HTTP Client", () => {
	// ...
});

class SpyServer {
}
2
Before you can factor out the SpyServer’s methods, you’ll need to use it in your test.
Instantiate it with the new keyword.
it("performs request", async () => {
	const spyServer = new SpyServer();
	const server = http.createServer();

	await new Promise((resolve, reject) => {  // add <void> for TypeScript
		server.listen(PORT);
		server.on("listening", () => {
			console.log("SERVER LISTENING");
			return resolve();
		});
	});
	console.log("SERVER STARTED");


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

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

		serverRequest.on("end", () => {
			console.log("SERVER RECEIVED ENTIRE REQUEST");
			console.log("SERVER RECEIVED METHOD:", serverRequest.method);
			console.log("SERVER RECEIVED PATH:", serverRequest.url);
			console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
			console.log("SERVER RECEIVED BODY:", body);

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

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


	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) => {  // add <HttpClientResponse> for TypeScript
		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,
				});
			});
		});
	});
	console.log("EXCHANGE COMPLETE");

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


	await new Promise((resolve, reject) => {  // add <void> for TypeScript
		server.close();
		server.on("close", () => {
			console.log("SERVER CLOSED");
			return resolve();
		});
	});
	console.log("SERVER STOPPED");
});
3
Now you’re ready to factor out startAsync(). Remember to use the async keyword.
You can move the startup code and the server.on("request", ...) listener.
It might be easiest to use your editor’s automatic refactoring to convert it to a function, then manually move it into the class.
it("performs request", async () => {
	const spyServer = new SpyServer();
	const server = await spyServer.startAsync();


	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) => {  // add <HttpClientResponse> for TypeScript
		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,
				});
			});
		});
	});
	console.log("EXCHANGE COMPLETE");

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


	await new Promise((resolve, reject) => {  // add <void> for TypeScript
		server.close();
		server.on("close", () => {
			console.log("SERVER CLOSED");
			return resolve();
		});
	});
	console.log("SERVER STOPPED");
});

// ...

class SpyServer {

	async startAsync() {
		const server = http.createServer();

		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			server.listen(PORT);
			server.on("listening", () => {
				console.log("SERVER LISTENING");
				return resolve();
			});
		});
		console.log("SERVER STARTED");

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

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

			serverRequest.on("end", () => {
				console.log("SERVER RECEIVED ENTIRE REQUEST");
				console.log("SERVER RECEIVED METHOD:", serverRequest.method);
				console.log("SERVER RECEIVED PATH:", serverRequest.url);
				console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
				console.log("SERVER RECEIVED BODY:", body);

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

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

		return server;
	}

}
4
Now you can factor out stopAsync().
Move the shutdown code.
As before, this might be easiest to do in two steps.
it("performs request", async () => {
	const spyServer = new SpyServer();
	const server = await spyServer.startAsync();


	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) => {  // add <HttpClientResponse> for TypeScript
		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,
				});
			});
		});
	});
	console.log("EXCHANGE COMPLETE");

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

	await spyServer.stopAsync(server);
});

// ...

class SpyServer {

	async startAsync() {
		const server = http.createServer();

		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			server.listen(PORT);
			server.on("listening", () => {
				console.log("SERVER LISTENING");
				return resolve();
			});
		});
		console.log("SERVER STARTED");


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

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

			serverRequest.on("end", () => {
				console.log("SERVER RECEIVED ENTIRE REQUEST");
				console.log("SERVER RECEIVED METHOD:", serverRequest.method);
				console.log("SERVER RECEIVED PATH:", serverRequest.url);
				console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
				console.log("SERVER RECEIVED BODY:", body);

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

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

	async stopAsync(server) {
		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			server.close();
			server.on("close", () => {
				console.log("SERVER CLOSED");
				return resolve();
			});
		});
		console.log("SERVER STOPPED");
	}

}

TypeScript needs a type declaration in stopAsync():

async stopAsync(server: http.Server) {
	await new Promise<void>((resolve, reject) => {
		server.close();
		server.on("close", () => {
			console.log("SERVER CLOSED");
			return resolve();
		});
	});
	console.log("SERVER STOPPED");
}
5
SpyServer should encapsulate Node’s server variable.
Convert it to an instance variable named this._server.
1. Search and replace server with this._server in SpyServer.
2. Remove server from the test and method signatures.
it("performs request", async () => {
	const spyServer = new SpyServer();
	await spyServer.startAsync();


	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) => {  // add <HttpClientResponse> for TypeScript
		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,
				});
			});
		});
	});
	console.log("EXCHANGE COMPLETE");

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

	await spyServer.stopAsync();
});

// ...

class SpyServer {

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

		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			this._server.listen(PORT);
			this._server.on("listening", () => {
				console.log("SERVER LISTENING");
				return resolve();
			});
		});
		console.log("SERVER STARTED");


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

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

			serverRequest.on("end", () => {
				console.log("SERVER RECEIVED ENTIRE REQUEST");
				console.log("SERVER RECEIVED METHOD:", serverRequest.method);
				console.log("SERVER RECEIVED PATH:", serverRequest.url);
				console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
				console.log("SERVER RECEIVED BODY:", body);

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

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

		// remove return
	}

	async stopAsync() {
		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			this._server.close();
			this._server.on("close", () => {
				console.log("SERVER CLOSED");
				return resolve();
			});
		});
		console.log("SERVER STOPPED");
	}

}

TypeScript requires several additional changes to make the type checker happy:

class SpyServer {

	_server?: http.Server;   // declare the _server instance variable

	async startAsync() {
		await new Promise<void>((resolve, reject) => {   // move everything inside the promise
			this._server = http.createServer();

			this._server.listen(PORT);
			this._server.on("listening", () => {
				console.log("SERVER LISTENING");
				return resolve();
			});

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

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

				serverRequest.on("end", () => {
					console.log("SERVER RECEIVED ENTIRE REQUEST");
					console.log("SERVER RECEIVED METHOD:", serverRequest.method);
					console.log("SERVER RECEIVED PATH:", serverRequest.url);
					console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
					console.log("SERVER RECEIVED BODY:", body);

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

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

	async stopAsync() {
		await new Promise<void>((resolve, reject) => {
			// add a guard clause to narrow the _server type
			if (this._server === undefined) return reject(new Error("SpyServer has not been started"));

			this._server.close();
			this._server.on("close", () => {
				console.log("SERVER CLOSED");
				return resolve();
			});
		});
		console.log("SERVER STOPPED");
	}

}
6
Move the startup and shutdown into before() and after() blocks.
You’ll need to declare spyServer separately from initializing it.
describe.only("HTTP Client", () => {

	let spyServer;   // Add ': SpyServer' for TypeScript

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

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

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

		it("performs request", async () => {
			// remove startup

			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) => {  // add <HttpClientResponse> for TypeScript
				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,
						});
					});
				});
			});
			console.log("EXCHANGE COMPLETE");

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

			// remove shutdown
		});

	// ...
});
7
All that’s left is to clean up.
Remove the startup and shutdown logging and unnecessary whitespace.
class SpyServer {

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

		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			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", () => {
				console.log("SERVER RECEIVED ENTIRE REQUEST");
				console.log("SERVER RECEIVED METHOD:", serverRequest.method);
				console.log("SERVER RECEIVED PATH:", serverRequest.url);
				console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
				console.log("SERVER RECEIVED BODY:", body);

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

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

	async stopAsync() {
		await new Promise((resolve, reject) => {  // add <void> for TypeScript
			this._server.close();
			this._server.on("close", () => resolve());
		});
	}

}

Complete Solution

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

	let spyServer;

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

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

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

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

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

	// ...
});


class SpyServer {

	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", () => {
				console.log("SERVER RECEIVED ENTIRE REQUEST");
				console.log("SERVER RECEIVED METHOD:", serverRequest.method);
				console.log("SERVER RECEIVED PATH:", serverRequest.url);
				console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
				console.log("SERVER RECEIVED BODY:", body);

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

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

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

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

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

		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<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,
						});
					});
				});
			});
			console.log("EXCHANGE COMPLETE");

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

	// ...
});


class SpyServer {

	_server?: http.Server;

	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", () => {
					console.log("SERVER RECEIVED ENTIRE REQUEST");
					console.log("SERVER RECEIVED METHOD:", serverRequest.method);
					console.log("SERVER RECEIVED PATH:", serverRequest.url);
					console.log("SERVER RECEIVED HEADERS:", serverRequest.headers);
					console.log("SERVER RECEIVED BODY:", body);

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

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

	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