Challenge #4: Request Tracking

In the real world, you’d probably take care of edge cases next. But you’ve seen that sort of code before. Instead, we’ll jump ahead to making the Rot13Client testable.

There are three aspects to making Rot13Client testable: tracking requests, turning off communication with the ROT-13 service, and configuring responses. You’ll start by tracking requests. This involves implementing the Output Tracking pattern.

Instructions

1. Implement the "tracks requests" test:

  1. Modify the transformAsync() helper to call rot13Client.trackRequests() and return its result in the { rot13Requests } variable.
  2. Make the following rot13Client request:
    • port: 9999
    • text: "my text"
    • correlation ID: "my-correlation-id"
  3. Assert that rot13Requests.data contains the following array of objects:
    [{
    	port: 9999,
    	text: "my text",
    	correlationId: "my-correlation-id",
    }]

2. Implement rot13Client.trackRequests():

  1. Initialize an OutputListener in the constructor.
  2. Return an OutputTracker from rot13Client.trackRequests().
  3. Track the request in rot13Client.transformAsync() by emitting an event to the listener.

Remember to commit your changes when you’re done.

API Documentation

const listener = OutputListener.create();

OutputListener is a utility class for implementing the “Output Tracking” pattern. Use it like this:

  1. Instantiate the OutputListener in your class’s constructor.
  2. Provide a trackXxx() method for consumers of your class to call.
  3. In your trackXxx() method, return listener.trackOutput(). That will create an OutputTracker that your class’s consumers can use to retrieve the tracked output.
  4. Track output by calling listener.emit().
  • returns listener (object) - the listener
listener.emit(data);

Inform the OutputListener of some data to be tracked. The listener will copy the data to every OutputTracker created from this listener. Does nothing if there are no OutputTrackers.

  • data (any): the data to be tracked
const outputTracker = listener.trackOutput();

Create an OutputTracker that records data that’s emitted to the listener. Each tracker is independent.

  • returns outputTracker (object) - the tracker
const output = outputTracker.data;

Returns an array with all data stored in the output tracker. Use it like this:

const rot13Requests = rot13Client.trackRequests();
// run code that makes requests
const output = rot13Requests.data;
  • returns output (array): the data
assert.deepEqual(actual, expected);

Assert that two objects (or arrays) and all their recursive contents are equal.

  • actual (any) - the actual value
  • expected (any) - the expected value

TypeScript Types

listener: OutputListener<Rot13ClientOutput>

The ROT-13 data to track.

interface Rot13ClientOutput {
	port: number,
	text: string,
	correlationId: string,
	cancelled?: boolean,    // not relevant to this challenge
}

JavaScript Primers

No new concepts.

Hints

Implement the "tracks requests" test:

1
You’ll need your transformAsync() test helper to support tracking requests.
Call rot13Client.trackRequests() in the helper.
You have to call trackRequests() before any requests are made.
async function transformAsync({
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
} = {}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	const rot13Client = new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const response = await rot13Client.transformAsync(port, text, correlationId);

	return { response, rot13Requests, httpRequests };
}
2
You’re ready to write the test.
Call the transformAsync() test helper and store the rot13Requests in a variable. Don’t forget to await it.
Assert that the requests are correct.
You can get the requests with rot13Requests.data and you can assert on them with assert.deepEqual().
it("tracks requests", async () => {
	const { rot13Requests } = await transformAsync({
		port: 9999,
		text: "my text",
		correlationId: "my-correlation-id",
	});

	assert.deepEqual(rot13Requests.data, [{
		port: 9999,
		text: "my text",
		correlationId: "my-correlation-id",
	}]);
});
3
The test is ready to run.
It should fail, saying that undefined prevented it from reading data. That means rot13Requests is undefined. (In TypeScript, it will say the requests are an empty array.)

That’s because rot13Client.trackRequests() isn’t implemented.

Implement the production code:

4
rot13Client.trackRequests() is supposed to return an OutputTracker. In order to do that, you need an OutputListener.
You can create an OutputListener with OutputListener.create().
It needs to be an instance variable, so create it in the constructor.
constructor(httpClient) {
	ensure.signature(arguments, [ HttpClient ]);

	this._httpClient = httpClient;
	this._listener = OutputListener.create();
}

In TypeScript, you’ll need to declare the type of the _listener instance variable:

export class Rot13Client {

	readonly _listener: OutputListener<Rot13ClientOutput>;

	// ...

	constructor(private readonly _httpClient: HttpClient) {
		this._listener = OutputListener.create();
	}
5
Now you can implement rot13Client.trackRequests().
You need to return an OutputTracker.
You can create an OutputTracker by calling this._listener.createTracker().
trackRequests() {
	return this._listener.trackOutput();
}
6
You’re ready to run the test again.
It should fail, saying that no requests were tracked (it got an empty array).

That’s because your production code isn’t telling the listener when requests occur.

7
Emit the tracking data when a request is made in your production code.
The request is made in rot13Client.transformAsync().
You can emit the event with this._listener.emit().
async transformAsync(port, text, correlationId) {
	this._listener.emit({ port, text, correlationId });

	const response = await this._httpClient.requestAsync({
		host: HOST,
		port,
		method: "POST",
		path: TRANSFORM_ENDPOINT,
		headers: {
			"content-type": "application/json",
			"x-correlation-id": correlationId,
		},
		body: JSON.stringify({ text }),
	});

	const parsedBody = JSON.parse(response.body);
	return parsedBody.transformed;
}

Complete Solution

Test code:
it("tracks requests", async () => {
	const { rot13Requests } = await transformAsync({
		port: 9999,
		text: "my text",
		correlationId: "my-correlation-id",
	});

	assert.deepEqual(rot13Requests.data, [{
		port: 9999,
		text: "my text",
		correlationId: "my-correlation-id",
	}]);
});

// ...

async function transformAsync({
	port = IRRELEVANT_PORT,
	text = IRRELEVANT_TEXT,
	correlationId = IRRELEVANT_CORRELATION_ID,
	rot13ServiceStatus = VALID_ROT13_STATUS,
	rot13ServiceHeaders = VALID_ROT13_HEADERS,
	rot13ServiceBody = VALID_ROT13_BODY,
} = {}) {
	const httpClient = HttpClient.createNull({
		"/rot13/transform": {
			status: rot13ServiceStatus,
			headers: rot13ServiceHeaders,
			body: rot13ServiceBody,
		},
	});
	const httpRequests = httpClient.trackRequests();

	const rot13Client = new Rot13Client(httpClient);
	const rot13Requests = rot13Client.trackRequests();

	const response = await rot13Client.transformAsync(port, text, correlationId);

	return { response, rot13Requests, httpRequests };
}
Production code (JavaScript):
export class Rot13Client {
	// ...

	constructor(httpClient) {
		ensure.signature(arguments, [ HttpClient ]);

		this._httpClient = httpClient;
		this._listener = OutputListener.create();
	}

	async transformAsync(port, text, correlationId) {
		this._listener.emit({ port, text, correlationId });

		const response = await this._httpClient.requestAsync({
			host: HOST,
			port,
			method: "POST",
			path: TRANSFORM_ENDPOINT,
			headers: {
				"content-type": "application/json",
				"x-correlation-id": correlationId,
			},
			body: JSON.stringify({ text }),
		});

		const parsedBody = JSON.parse(response.body);
		return parsedBody.transformed;
	}

	trackRequests() {
		return this._listener.trackOutput();
	}

	// ...
}
Production code (TypeScript):
export class Rot13Client {

	readonly _listener: OutputListener<Rot13ClientOutput>;

	// ...

	constructor(private readonly _httpClient: HttpClient) {
		this._listener = OutputListener.create();
	}

	async transformAsync(
		port: number,
		text: string,
		correlationId: string,
	): Promise<string> {
		this._listener.emit({ port, text, correlationId });

		const response = await this._httpClient.requestAsync({
			host: HOST,
			port,
			method: "POST",
			path: TRANSFORM_ENDPOINT,
			headers: {
				"content-type": "application/json",
				"x-correlation-id": correlationId,
			},
			body: JSON.stringify({ text }),
		});

		const parsedBody = JSON.parse(response.body);
		return parsedBody.transformed;
	}

	trackRequests(): OutputTracker<Rot13ClientOutput> {
		return this._listener.trackOutput();
	}

	// ...
}

Next challenge

Return to module overview