Skip to content

Commit c750654

Browse files
committed
Tests with real MQ
1 parent 20300c6 commit c750654

11 files changed

+283
-139
lines changed

example-application/data-access/order-repository.js

-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,6 @@ module.exports = class OrderRepository {
4848
}
4949

5050
async deleteOrder(orderToDelete) {
51-
console.log('About to delete', orderToDelete);
5251
await orderModel.destroy({ where: { id: orderToDelete } });
5352
return;
5453
}

example-application/entry-points/api.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ const defineRoutes = (expressApp) => {
9696
}
9797

9898
// We should notify others that a new order was added - Let's put a message in a queue
99-
new MessageQueueClient().sendMessage('new-order', req.body);
99+
await new MessageQueueClient().publish('order.events','order.events.new', req.body);
100100

101101
res.json(DBResponse);
102102
} catch (error) {

example-application/entry-points/message-queue-starter.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,17 @@ class MessageQueueStarter {
1414
}
1515

1616
async consumeUserDeletionQueue() {
17-
console.log("consume register", this.queueName)
1817
// Let's now register to new delete messages from the queue
1918
return await this.messageQueueClient.consume(
2019
this.queueName,
2120
async (message) => {
21+
2222
// Validate to ensure it is not a poisoned message (invalid) that will loop into the queue
2323
const newMessageAsObject = JSON.parse(message);
2424

2525
// ️️️✅ Best Practice: Validate incoming MQ messages using your validator framework (simplistic implementation below)
2626
if (!newMessageAsObject.id) {
27-
throw new AppError('invalid-message', true);
27+
throw new AppError('invalid-message', 'Unknown message schema');
2828
}
2929

3030
const orderRepository = new OrderRepository();

example-application/libraries/fake-message-queue-provider.js

+6-6
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,24 @@ class FakeMessageQueueProvider extends EventEmitter {
2020
this.emit('message-sent', message);
2121
}
2222

23+
async publish(exchangeName, routingKey, message) {
24+
this.emit('message-published', message);
25+
this.pushMessageToQueue('unknown', message);
26+
}
27+
2328
async assertQueue() {}
2429

2530
async consume(queueName, messageHandler) {
2631
// We just save the callback (handler) locally, whenever a message will put into this queue
2732
// we will fire this handler
28-
console.info(`Received request to listen to the queue ${queueName}`);
29-
console.log("6");
3033
this.messageHandler = messageHandler;
3134
}
3235

3336
// This is the only method that does not exist in the MQ client library
3437
// It allows us to fake like there is a new message in the queue and start a flow
3538
async pushMessageToQueue(queue, newMessage) {
3639
if (this.messageHandler) {
37-
const wrappedMessage = {
38-
content: Buffer.from(JSON.stringify(newMessage)),
39-
};
40-
this.messageHandler(wrappedMessage);
40+
this.messageHandler({content: newMessage})
4141
} else {
4242
// Just warning and no exception because the test might want to simulate that
4343
console.error(

example-application/libraries/message-queue-client.js

+47-32
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const amqplib = require('amqplib');
22
const { EventEmitter } = require('events');
3+
const { throwIfEmpty } = require('rxjs/operators');
34
const { AppError, errorHandler } = require('../error-handling');
45
const { FakeMessageQueueProvider } = require('./fake-message-queue-provider');
56

@@ -9,12 +10,17 @@ class MessageQueueClient extends EventEmitter {
910
constructor(customMessageQueueProvider) {
1011
super();
1112
this.isReady = false;
13+
this.requeue = true; // Tells whether to return failed messages to the queue
1214

1315
// To facilitate testing, the client allows working with a fake MQ provider
1416
// It can get one in the constructor here or even change by environment variables
1517
if (customMessageQueueProvider) {
1618
this.messageQueueProvider = customMessageQueueProvider;
17-
} else {
19+
}
20+
else if(process.env.USE_FAKE_MQ === 'true'){
21+
this.messageQueueProvider = new FakeMessageQueueProvider();
22+
}
23+
else {
1824
this.messageQueueProvider = amqplib;
1925
}
2026

@@ -63,14 +69,14 @@ class MessageQueueClient extends EventEmitter {
6369
if (!this.channel) {
6470
await this.connect();
6571
}
66-
console.log('publish', exchangeName, routingKey);
6772

6873
const sendResponse = await this.channel.publish(
6974
exchangeName,
7075
routingKey,
7176
Buffer.from(JSON.stringify(message)),
7277
{ messageId }
7378
);
79+
this.emit('publish', { exchangeName, routingKey, message });
7480

7581
return sendResponse;
7682
}
@@ -80,7 +86,6 @@ class MessageQueueClient extends EventEmitter {
8086
await this.connect();
8187
}
8288
const queueDeletionResult = await this.channel.deleteQueue(queueName);
83-
console.log(queueDeletionResult);
8489

8590
return;
8691
}
@@ -118,20 +123,17 @@ class MessageQueueClient extends EventEmitter {
118123
await this.connect();
119124
}
120125
this.channel.assertQueue(queueName);
121-
console.log('consume start', queueName);
122126

123127
await this.channel.consume(queueName, async (theNewMessage) => {
124128
//Not awaiting because some MQ client implementation get back to fetch messages again only after handling a message
125129
onMessageCallback(theNewMessage.content.toString())
126130
.then(() => {
127-
console.log('ack');
128131
this.emit('ack', theNewMessage);
129132
this.channel.ack(theNewMessage);
130133
})
131134
.catch((error) => {
132-
this.channel.nack(theNewMessage, false, true);
135+
this.channel.nack(theNewMessage, false, this.requeue);
133136
this.emit('nack', theNewMessage);
134-
console.log('nack', error.message);
135137
error.isTrusted = true; //Since it's related to a single message, there is no reason to let the process crash
136138
//errorHandler.handleError(error);
137139
});
@@ -140,38 +142,51 @@ class MessageQueueClient extends EventEmitter {
140142
return;
141143
}
142144

145+
setRequeue(newValue) {
146+
this.requeue = newValue;
147+
}
148+
149+
// This function stores all the MQ events in a local data structure so later
150+
// one query this
143151
countEvents() {
144-
const eventsToListen = ['nack', 'ack'];
145-
if (this.eventsCounter === undefined) {
146-
this.eventsCounter = {};
147-
eventsToListen.forEach((eventToListenTo) => {
148-
this.eventsCounter[eventToListenTo] = 0;
149-
this.on(eventToListenTo, (eventData) => {
150-
this.eventsCounter[eventToListenTo]++;
151-
console.log('events counting', this.eventsCounter);
152-
this.emit('event-counted', {
153-
name: eventToListenTo,
154-
lastEventData: eventData,
155-
count: this.eventsCounter[eventToListenTo],
156-
});
152+
const eventsToListen = ['nack', 'ack', 'publish'];
153+
if (this.eventsRecorder !== undefined) {
154+
return; // Already initialized and set up
155+
}
156+
this.eventsRecorder = {};
157+
eventsToListen.forEach((eventToListenTo) => {
158+
this.eventsRecorder[eventToListenTo] = {
159+
count: 0,
160+
lastEventData: null,
161+
name: eventToListenTo,
162+
};
163+
this.on(eventToListenTo, (eventData) => {
164+
this.eventsRecorder[eventToListenTo].count++;
165+
this.eventsRecorder[eventToListenTo].lastEventData = eventData;
166+
this.emit('message-queue-event', {
167+
name: eventToListenTo,
168+
eventsRecorder: this.eventsRecorder,
157169
});
158170
});
159-
}
171+
});
160172
}
161173

162-
// Helper methods for testing
174+
resolveIfEventExceededThreshold(eventName, threshold, resolve) {
175+
if (this.eventsRecorder[eventName].count >= threshold) {
176+
resolve({
177+
name: eventName,
178+
lastEventData: this.eventsRecorder[eventName].lastEventData,
179+
count: this.eventsRecorder[eventName].count,
180+
});
181+
}
182+
}
183+
// Helper methods for testing - Resolves/fires when some event happens
163184
async waitFor(eventName, howMuch) {
164185
return new Promise((resolve, reject) => {
165-
this.on('event-counted', (eventInfo) => {
166-
if (eventInfo.name !== eventName) {
167-
return;
168-
}
169-
if (eventInfo.count >= howMuch) {
170-
resolve({
171-
lastEventData: eventInfo.lastEventData,
172-
count: eventInfo.count,
173-
});
174-
}
186+
// The first resolve is for cases where the caller has approached AFTER the event has already happen
187+
this.resolveIfEventExceededThreshold(eventName, howMuch, resolve);
188+
this.on('message-queue-event', (eventInfo) => {
189+
this.resolveIfEventExceededThreshold(eventName, howMuch, resolve);
175190
});
176191
});
177192
}

recipes/message-queue/benchmark-queue-creation-time.js

-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ const {
1717
const numOfIterations = 200;
1818

1919
for (let index = 0; index < numOfIterations; index++) {
20-
console.time('measure');
2120
const startTime = new Date();
2221
const userDeletedQueueName = `user-deleted-${getShortUnique()}`;
2322
await mqClient.assertQueue(userDeletedQueueName);
@@ -31,9 +30,6 @@ const {
3130
const endTime = new Date();
3231
const singleTime = endTime.getTime() - startTime.getTime();
3332
overallTime += singleTime;
34-
console.log('Single time', singleTime);
35-
console.timeEnd('measure');
3633
}
3734

38-
console.log('Overall time', overallTime / numOfIterations);
3935
})();

recipes/message-queue/fake-message-queue.test.js

+36-30
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
const axios = require('axios');
22
const sinon = require('sinon');
33
const nock = require('nock');
4-
const { once } = require('events');
4+
const testHelpers = require('./test-helpers');
5+
56
const {
67
getNextMQConfirmation,
78
startFakeMessageQueue,
89
} = require('./test-helpers');
9-
const {
10-
FakeMessageQueueProvider,
11-
} = require('../../example-application/libraries/fake-message-queue-provider');
12-
1310
const {
1411
initializeWebServer,
1512
stopWebServer,
1613
} = require('../../example-application/entry-points/api');
14+
const MessageQueueClient = require('../../example-application/libraries/message-queue-client');
1715

1816
let axiosAPIClient;
1917

@@ -30,6 +28,8 @@ beforeAll(async (done) => {
3028
};
3129
axiosAPIClient = axios.create(axiosConfig);
3230

31+
process.env.USE_FAKE_MQ = 'true';
32+
3333
done();
3434
});
3535

@@ -51,46 +51,55 @@ afterAll(async (done) => {
5151
await stopWebServer();
5252
//await messageQueueClient.close();
5353
nock.enableNetConnect();
54+
process.env.USE_FAKE_MQ = undefined;
5455
done();
5556
});
5657

5758
// ️️️✅ Best Practice: Test a flow that starts via a queue message and ends with removing/confirming the message
5859
test('Whenever a user deletion message arrive, then his orders are deleted', async () => {
5960
// Arrange
60-
const orderToAdd = {
61-
userId: 1,
62-
productId: 2,
63-
mode: 'approved',
64-
};
65-
const addedOrderId = (await axiosAPIClient.post('/order', orderToAdd)).data
66-
.id;
67-
const fakeMessageQueue = await startFakeMessageQueue();
68-
const getNextMQEvent = getNextMQConfirmation(fakeMessageQueue); //Store the MQ actions in a promise
61+
const orderToAdd = {
62+
userId: 1,
63+
productId: 2,
64+
mode: 'approved',
65+
};
66+
const addedOrderId = (await axiosAPIClient.post('/order', orderToAdd)).data
67+
.id;
68+
const messageQueueClient = await testHelpers.startMQSubscriber(
69+
'fake',
70+
'user.deleted'
71+
);
6972

7073
// Act
71-
fakeMessageQueue.pushMessageToQueue('deleted-user', { id: addedOrderId });
74+
await messageQueueClient.publish('user.events', 'user.deleted', {
75+
id: addedOrderId,
76+
});
7277

7378
// Assert
74-
const eventFromMessageQueue = await getNextMQEvent;
75-
expect(eventFromMessageQueue).toEqual([{ event: 'message-acknowledged' }]);
79+
await messageQueueClient.waitFor('ack', 1);
7680
const aQueryForDeletedOrder = await axiosAPIClient.get(
7781
`/order/${addedOrderId}`
7882
);
7983
expect(aQueryForDeletedOrder.status).toBe(404);
8084
});
8185

82-
test.only('When a poisoned message arrives, then it is being rejected back', async () => {
86+
test('When a poisoned message arrives, then it is being rejected back', async () => {
8387
// Arrange
8488
const messageWithInvalidSchema = { nonExistingProperty: 'invalid' };
85-
const fakeMessageQueue = await startFakeMessageQueue();
86-
const getNextMQEvent = getNextMQConfirmation(fakeMessageQueue);
89+
const messageQueueClient = await testHelpers.startMQSubscriber(
90+
'fake',
91+
'user.deleted'
92+
);
8793

8894
// Act
89-
fakeMessageQueue.pushMessageToQueue('deleted-user', messageWithInvalidSchema);
95+
await messageQueueClient.publish(
96+
'user.events',
97+
'user.deleted',
98+
messageWithInvalidSchema
99+
);
90100

91101
// Assert
92-
const eventFromMessageQueue = await getNextMQEvent;
93-
expect(eventFromMessageQueue).toEqual([{ event: 'message-rejected' }]);
102+
await messageQueueClient.waitFor('nack', 1);
94103
});
95104

96105
// ️️️✅ Best Practice: Verify that messages are put in queue whenever the requirements state so
@@ -101,17 +110,14 @@ test('When a valid order is added, then a message is emitted to the new-order qu
101110
productId: 2,
102111
mode: 'approved',
103112
};
104-
const spyOnSendMessage = sinon.stub(
105-
FakeMessageQueueProvider.prototype,
106-
'sendToQueue'
107-
);
113+
const spyOnSendMessage = sinon.spy(MessageQueueClient.prototype, 'publish');
108114

109115
//Act
110116
await axiosAPIClient.post('/order', orderToAdd);
111117

112118
// Assert
113-
expect(spyOnSendMessage.called).toBe(true);
114-
expect(spyOnSendMessage.lastCall.args).toMatchObject({}); //TODO - Be more explicit here
119+
expect(spyOnSendMessage.lastCall.args[0]).toBe('order.events');
120+
expect(spyOnSendMessage.lastCall.args[1]).toBe('order.events.new');
115121
});
116122

117123
test.todo('When an error occurs, then the message is not acknowledged');
@@ -129,4 +135,4 @@ test.todo(
129135
);
130136
test.todo(
131137
'When multiple user deletion message arrives and one fails, then only the failed message is not acknowledged'
132-
);
138+
);

0 commit comments

Comments
 (0)