Challenge #10: Exceptional Responses
What happens if an endpoint is configured with an array of responses and the array runs out? In this challenge, you’ll make the code fail fast with an informative error.
Instructions
1. In the "throws exception when list of configured responses runs out"
test:
- Configure the Nulled
HttpClient
with an endpoint that has one response. - Assert that
http.requestAsync()
throws a helpful error the second time it’s called on that endpoint.
2. In the production code:
- Modify the embedded stub to make the test pass.
Remember to commit your changes when you’re done.
API Documentation
await assert.doesNotThrowAsync(fnAsync);
Assert that an asynchronous function does not throw an error. Use it like this:
await assert.doesNotThrowAsync(
() => transformAsync(...), // note: NO await here
);
- fnAsync
(function)
- the function to run
await assert.throwsAsync(fnAsync, expectedError);
Assert that an asynchronous function throws an error. Use it like this:
await assert.throwsAsync(
() => transformAsync(...), // note: NO await here
"my expected error",
);
- fnAsync
(function)
- the function to run - expectedError
(string | regex)
- the errorfnAsync
is expected to throw
const length = array.length;
Get the number of entries in an array.
- returns length
(number)
- the number of entries
JavaScript Primers
No new concepts.
Hints
1
The test needs to configure an endpoint with a single response.
Remember that endpoints configured without an array have an infinite number of responses.
To configure a single response, you have to put it in an array.
it("throws exception when list of configured responses runs out", async () => {
const client = HttpClient.createNull({
"/endpoint": [{ status: 200, body: "my body" }],
});
});
2
Assert that the first request doesn’t throw an exception.
You can use assert.doesNotThrowAsync()
for that. Don’t forget to await
it.
it("throws exception when list of configured responses runs out", async () => {
const client = HttpClient.createNull({
"/endpoint": [{ status: 200, body: "my body" }],
});
await assert.doesNotThrowAsync(
() => requestAsync({ client, path: "/endpoint" }),
);
});
3
Assert that the second request does throw an exception.
You can use assert.throwsAsync()
for that. Don’t forget to await
it.
it("throws exception when list of configured responses runs out", async () => {
const client = HttpClient.createNull({
"/endpoint": [{ status: 200, body: "my body" }],
});
await assert.doesNotThrowAsync(
() => requestAsync({ client, path: "/endpoint" }),
);
await assert.throwsAsync(
() => requestAsync({ client, path: "/endpoint" }),
"No more responses configured in Nulled HTTP client",
);
});
4
The test is ready to run.
It should fail with "expected exception"
.
That’s because your code isn’t throwing an exception when it runs out of items.
5
Modify your production code to throw an exception when the array runs out.
You can tell how many entries are left in the array with array.length
.
The nextResponse()
function is a good place to put it.
function nextResponse(responses) {
if (Array.isArray(responses)) {
if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
return responses.shift();
}
else {
return responses;
}
}
6
When you run the test, it fails! Why? Isn’t the assertion supposed to catch the exception?
This one is tricky. Take a close look at the stack trace.
The test isn’t in the stack trace.
The exception is being thrown inside setImmediate()
.
setImmediate()
schedules code to be run in the future.
Because the exception is being thrown inside setImmediate()
, it has its own stack frame, which means that
assert.throwsAsync()
doesn’t get the exception. To prevent the problem, the exception needs to be thrown outside of setImmediate()
.
7
Make the test pass.
Make sure the exception isn’t thrown inside setImmediate()
.
One way to do so is to move the call to nextResponse()
.
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
const response = nextResponse(this._responses);
setImmediate(() => {
this.emit("response", new StubbedResponse(response));
});
}
}
Complete Solution
Test code:
it("throws exception when list of configured responses runs out", async () => {
const client = HttpClient.createNull({
"/endpoint": [{ status: 200, body: "my body" }],
});
await assert.doesNotThrowAsync(
() => requestAsync({ client, path: "/endpoint" }),
);
await assert.throwsAsync(
() => requestAsync({ client, path: "/endpoint" }),
"No more responses configured in Nulled HTTP client",
);
});
Production code (JavaScript):
class StubbedRequest extends EventEmitter {
constructor(responses) {
super();
this._responses = responses;
}
end() {
const response = nextResponse(this._responses);
setImmediate(() => {
this.emit("response", new StubbedResponse(response));
});
}
}
function nextResponse(responses) {
if (Array.isArray(responses)) {
if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
return responses.shift();
}
else {
return responses;
}
}
Production code (TypeScript):
class StubbedRequest extends EventEmitter {
constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
super();
}
end() {
const response = nextResponse(this._responses);
setImmediate(() => {
this.emit("response", new StubbedResponse(response));
});
}
}
function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
if (Array.isArray(responses)) {
if (responses.length === 0) throw new Error("No more responses configured in Nulled HTTP client");
return responses.shift();
}
else {
return responses;
}
}