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:
- 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 }
- Convert the request logging to an assertion.
- Add a
spyServer.reset()
method that resetsspyServer.lastRequest
tonull
. - Add a
beforeEach()
function that callsspyServer.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.