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:
- Create a
SpyServer
class. - Move the startup and request handling code into
SpyServer.startAsync()
. - Move the shutdown code into
SpyServer.stopAsync()
. - Add a
before()
function that starts the spy server before any of the tests run. Put it inside the top-leveldescribe()
block. - Add an
after()
function that stops the spy server after all the tests run. Put it inside the top-leveldescribe()
block. - 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.