README
Eventest
The purpose of this library is to enable a simple route to run end to end tests for Event Driven Systems.
The name is a portmanteau of Event and Test.
When creating web applications that have asynchronous backend systems it can be very challenging to test those backends. In an e-commerce example: It would be useful to test that putting a new Order into the Order Service causes:
- Website sends a NewReservationRequest through a POST to the Reservation Service
- Reservation Service generates a
NewReservationReceived
event. - Reservation Service issues
TakePayment
command to the Payment Service - Payment Service issues
Payment Taken Event
- Reservation Service issues a
ReservationConfirmed
event
Eventest contains a set of components to make testing for these conditions as simple as writing a Unit Test.
The basic assumption of the package is that you're system follows the good (perhaps best) practice of propagating the Correlation Id from the initial request or message through all the subsequent requests, responses, and messages.
Components
There are 2 different sections to the library at the moment:
Broker
The Broker abstraction is the core part of the abstraction that allows you listen for, or publish, events and commands in your tests.
The Broker interface is the common abstraction, which concrete implementations given in dependent libraries that allows you to run against
real brokers, e.g. eventest.servicebus
which wraps Azure Service Bus with this instance.
Each test group should create a Broker instance. The Broker then creates a unique Id for the test run, which is used when subscribing to messages. This test Id will be used as the CorrelationId for all messages that the Broker sends, and all subscriptions that are issued against the broker will be filtered to only raise messages that have that Correlation Id. This means your tests will run independently in terms of messaging and wont get false postivites/negatives because they received unrelated messages.
You can send messages, Commands or Events, to the system through the sendAMessage(message:any, topicOrQueueName:string) : Promise<void>;
method.
Subscriptions
To subscribe to a messaging topics, the Broker has a subscribeToTopic(topicName:string) : Promise<Subscription>
method.
This Subscription will be filtered to only receive messages from this test.
Once you have a Subscription
object you can wait for messages to be delivered to that subscription.
The method waitForMessage(timeoutInMS: number): Promise<ReceiveResult>
will wait up to the timeout for a message to arrive.
The method definitely returns a ReceivedResult
object which enables you to determine whether a message arrived.
ReceiveResult
The ReceiveResult
is returned by the waitForMessage(timeoutInMS: number): Promise<ReceiveResult>
on a Subscription
.
You should check the didReceive
property to check if a message was actually received.
If that property returns true you can call the getMessageBody(): any
to get the contents of the message.
Http Abstraction
There is an http
abstraction too.
This can be used to post to and from the HTTP endpoints in your systems as triggers for event flows.
Posting to a REST web service
The method postToService(address: string, bodyData: any, headers?: any): Promise<Response>
allows you to post an object to a service.
Just pass the address, the object to POST, and any headers you want send to the method.
Getting from a web service
The method getFromService(address: string, headers?: any): Promise<Response>
allows you to perform a GET operation against a web service.
Just pass the address and any headers you want include in the request to the method.
Handling Responses
The Response
class is returned from these web methods and allows you to access the response from the server.
- The
success
property returnstrue
if the servies returns a status code between 200 and 299. - The
statusCode
property returns the actual status code that the service returned. - The
body
property returns the contents of the response deserialized into an object. - The
result
property returns the originalfetch()
response.
Example of use of this library:
This library uses Mocha to run these tests like unit tests. Example code:
describe('Submitting NewReservationRequest', async () => {
var test: Bus.Broker;
var NewReservationReceivedSubscription: Bus.Subscription;
var TakePaymentSubscription: Bus.Subscription;
var PaymentTakenSubscription: Bus.Subscription;
var ReservationConfirmedSubscription: Bus.Subscription;
var testReservationId = 123;
before(async () => {
// runs once before the first test in this block
//Create a Service Bus connection for this test
test = new AzureServiceBusTester(
process.env.SERVICEBUS_CONNECTION_STRING ?? "",
new MassTransitMessageEncoder()
);
//Subscribe to the topic first so we dont miss the messages
NewReservationReceivedSubscription = await test.subscribeToTopic("newreservationreceived");
TakePaymentSubscription = await test.subscribeToTopic("takepayment");
PaymentTakenSubscription = await test.subscribeToTopic("paymenttaken");
ReservationConfirmedSubscription = await test.subscribeToTopic("reservationconfirmed");
//give it a couple of seconds to make sure the subscriptions are active
delay(2000);
});
it('should get OK status', async () => {
var svcResponse = await http.postToService(process.env.SUBMIT_RESERVATION_SERVICE_ENDPOINT ?? "",
//This is the payload to send to the service:
{
RequestCorrelationId: test.testUniqueId,
ReservationId: testReservationId,
StartDate: moment().format('YYYY-MM-DDTHH:mm:ss'),
EndDate: moment().add("2","days").format('YYYY-MM-DDTHH:mm:ss'),
GuestId: 123
});
// test that we got a 200 success
expect(svcResponse.statusCode).equal(200);
});
it('should publish NewReservationEvent', async () => {
//wait up to 2 seconds to receive a message on our subscription
var receivedMessage = await NewReservationReceivedSubscription.waitForMessage(2000);
//test we got a message
expect(receivedMessage.didReceive).equal(true);
//test the reservation Id matches
expect(receivedMessage.getMessageBody().reservationId).equal(testReservationId);
});
it('should return the Reservation', async () => {
var result = await http.getFromService((process.env.GET_RESERVATION_SERVICE_ENDPOINT ?? "") + "?reservationId=" + testReservationId);
expect(result.success).equal(true);
});
it('should publish Take Payment Command', async () => {
var receivedMessage = await TakePaymentSubscription.waitForMessage(2000);
expect(receivedMessage.didReceive).equal(true);
});
it('should publish Payment Taken Event', async () => {
var receivedMessage = await PaymentTakenSubscription.waitForMessage(2000);
expect(receivedMessage.didReceive).to.equal(true);
});
it('should publish ReservationConfirmed event', async () => {
var receivedMessage = await ReservationConfirmedSubscription.waitForMessage(2000);
expect(receivedMessage.didReceive).equal(true);
});
it('should return the Reservation as State=Confirmed', async () => {
var result = await http.getFromService((process.env.GET_RESERVATION_SERVICE_ENDPOINT ?? "") + "?reservationId=" + testReservationId);
//test we got a 200 level response
expect(result.success).equal(true);
//test that the object in the body had a field called status with a value = 'Confirmed'
expect(result.body.Status).equal("Confirmed");
});
//Clean up after all the tests
after(async () => {
//this removes all the subscriptions we made
test.cleanup();
});
});
Example output:
Passing Tests:
When running locally on a console window (shown here in powershell):
It's also possible to run these as a CI/CD pipeline, perhaps after you've done an automated deployment of your application. This is a screen shot of run summary from Azure Devops:
and a list of tests: