Challenge #9: Distinct Responses

Some services will require the same endpoint to be called multiple times, returning a different response each time. In this challenge, you’ll update your embedded stub to support that type of behavior, but you’ll also continue to support the existing behavior:

If an endpoint is configured with an array, it will return the next response in the array each time a response is needed. Otherwise, if it’s configured with a single response (that’s not in an array), it will always return that response.

Instructions

1. In the "provides distinct responses when an endpoint has a list of responses configured" test:

  1. Configure the Nulled HttpClient to return multiple response, as follows:
    const client = HttpClient.createNull({
    	"/endpoint": [
    		{ status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
    		{ status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
    	],
    });
  2. Make multiple requests of /endpoint and assert that the responses match the configured values.

2. In the production code:

  1. Modify the embedded stub to make the test pass.

Remember to commit your changes when you’re done.

API Documentation

const isArray = Array.isArray(variable)

Returns true if variable is an array and false if it isn’t.

  • variable (any) - the variable to test
  • returns isArray (boolean) - whether the variable is an array
const element = array.shift()

Gets the first element from an array and removes it from the array.

  • array (array) - the array to modify
  • returns element (boolean) - the first element in the array

JavaScript Primers

No new concepts.

Hints

1
The test needs to configure the responses, then make multiple requests and assertions.
This is like the previous test.
it("provides distinct responses when an endpoint has a list of responses configured", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [
			{ status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
			{ status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
		],
	});

	const { response: response1 } = await requestAsync({ client, path: "/endpoint" });
	const { response: response2 } = await requestAsync({ client, path: "/endpoint" });

	assert.deepEqual(response1, {
		status: 200,
		headers: { myheader1: "myValue1" },
		body: "my body 1",
	});
	assert.deepEqual(response2, {
		status: 300,
		headers: { myheader2: "myValue2" },
		body: "my body 2",
	});
});
2
The test is ready to run.
It should fail, saying that status, headers, and body have the wrong values.

That’s because the stub is expecting an object, but the configuration is passing in an array.

3
StubbedRequest is no longer being configured with a single response.
It needs to get the next response from the list.
You can use this._responses.shift() to do that.
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

	end() {
		setImmediate(() => {
			const response = this._responses.shift();
			this.emit("response", new StubbedResponse(response));
		});
	}

}

This code won’t compile in TypeScript. The reason is discussed in the next hint.

4
In TypeScript, the code doesn’t compile. (If you’re using JavaScript, skip this hint.)
It should complain that this._responses is possibly undefined and that shift() does not exist.
The shift() error is the bigger issue.
It’s happening because StubbedHttp is using as NulledHttpClientResponse to hide the response’s true type, which is NulledHttpClientResponse | NulledHttpClientResponse[].
Remove the as clause and fix the types.
class StubbedHttp {

	constructor(private readonly _responses: NulledHttpClientResponses = {}) {
	}

	request({ path }: { path: string }) {
		return new StubbedRequest(this._responses[path]);  // removed "as NulledHttpClientResponse"
	}

}

class StubbedRequest extends EventEmitter {

	constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
		super();
	}

	end() {
		setImmediate(() => {
			const response = this._responses.shift();
			this.emit("response", new StubbedResponse(response));
		});
	}

}
5
In TypeScript, the code still doesn’t compile. (If you’re using JavaScript, skip this hint.)
It’s complaining for the same reasons, even though you added declared that this._responses could be an array.
But it’s not guaranteed to be an array.

The code still won’t compile because shift() is only available on arrays, and this._responses isn’t guaranteed to be an array.

6
In JavaScript, what happens if you run the tests? (If you’re using TypeScript, skip this hint.)
They should fail, complaining that this._responses.shift() is not a function. Why do they fail?
Look at the test names.
It’s the other nulled instance tests that are failing. The test you’re working on passed.

The tests are failing because their responses aren’t an array.

7
Modify the code to check if this._responses is an array.
You can use Array.isArray() for that.
If it’s not an array, you can use it as-is.
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

	end() {
		setImmediate(() => {
			let response;
			if (Array.isArray(this._responses)) {
				response = this._responses.shift();
			}
			else {
				response = this._responses;
			}

			this.emit("response", new StubbedResponse(response));
		});
	}

}
8
Refactor and clean up.
This is a matter of taste, with no right answer.
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

	end() {
		setImmediate(() => {
			const response = nextResponse(this._responses);
			this.emit("response", new StubbedResponse(response));
		});
	}

}

function nextResponse(responses) {
	if (Array.isArray(responses)) {
		return responses.shift();
	}
	else {
		return responses;
	}
}

TypeScript’s implementation of nextResponse() needs a type declaration:

function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
	if (Array.isArray(responses)) {
		return responses.shift();
	}
	else {
		return responses;
	}
}

Complete Solution

Test code:
it("provides distinct responses when an endpoint has a list of responses configured", async () => {
	const client = HttpClient.createNull({
		"/endpoint": [
			{ status: 200, headers: { myheader1: "myValue1" }, body: "my body 1" },
			{ status: 300, headers: { myheader2: "myValue2" }, body: "my body 2" },
		],
	});

	const { response: response1 } = await requestAsync({ client, path: "/endpoint" });
	const { response: response2 } = await requestAsync({ client, path: "/endpoint" });

	assert.deepEqual(response1, {
		status: 200,
		headers: { myheader1: "myValue1" },
		body: "my body 1",
	});
	assert.deepEqual(response2, {
		status: 300,
		headers: { myheader2: "myValue2" },
		body: "my body 2",
	});
});
Production code (JavaScript):
class StubbedRequest extends EventEmitter {

	constructor(responses) {
		super();
		this._responses = responses;
	}

	end() {
		setImmediate(() => {
			const response = nextResponse(this._responses);
			this.emit("response", new StubbedResponse(response));
		});
	}

}

function nextResponse(responses) {
	if (Array.isArray(responses)) {
		return responses.shift();
	}
	else {
		return responses;
	}
}
Production code (TypeScript):
class StubbedHttp {

	constructor(private readonly _responses: NulledHttpClientResponses = {}) {
	}

	request({ path }: { path: string }) {
		return new StubbedRequest(this._responses[path]);
	}

}

class StubbedRequest extends EventEmitter {

	constructor(private readonly _responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
		super();
	}

	end() {
		setImmediate(() => {
			const response = nextResponse(this._responses);
			this.emit("response", new StubbedResponse(response));
		});
	}

}

function nextResponse(responses?: NulledHttpClientResponse | NulledHttpClientResponse[]) {
	if (Array.isArray(responses)) {
		return responses.shift();
	}
	else {
		return responses;
	}
}

Next challenge

Return to module overview