Antman261 / eventspec

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Eventspec (WIP)

Eventspec is a behavioural contract testing framework for event-driven architectures in Nodejs.

Unlike typical contract testing (like Pact), Eventspec aims to formalise the hidden behavioural assumptions between services inherent in event-driven architectures.

These assumptions may be along the lines of:

  • After Service A emits event A, Service B will emit event B
  • After Service A emits event A, then event B, Service B will emit event C

Eventspec deals primarily in testing the correct sequence of events, rather than their exact payload.

Producers & Consumers

In an event driven architecture, both sides of a contract are typically both publishing events. We define the Producer as the service upon whose behaviour another service depends.

Tests are written by the consumer and executed by the producer. This allows the team working on the Producer service to understand their behavioural obligations to their consumers.

Use Eventspec alongside a contract testing framework like Pact. You will typically want a lot more schema contract tests than behavioural tests, as EDAs are typically used to minimise coupling. Your philosophy for using Eventspec should be to use as little as possible to protect core business value generating behaviours.

Example: Consumer spec

service('serviceB')
  .dependOn('serviceA', () => {
  describe('label cancellation', () => {
    it('allows a pre-booked label to be cancelled', () => {
      send('PARCEL TYPE SELECTED');
      receive('LABEL BOOKED');
      send('ORDER REJECTED');
      receive('LABEL CANCELLED'); 
    });
    it('does not cancel a label after it has been picked up', () => {
      send('PARCEL TYPE SELECTED');
      receive('LABEL BOOKED');
      send('SHIPMENT PICKED UP', { from: 'serviceC' });
      send('ORDER REJECTED');
      doNotReceive('LABEL CANCELLED'); // default timeout 10s 
    })
  })
})

The test case above:

  • Declares that service B depends on service A: Service A will execute the test written by service B
  • specifies events that will be sent by the consumer service (B), and events it expects in return
  • specifies an event sent by another service (C)
  • specifies an event it expects not to receive

We define the events used in our specs separately as follows:

events(() => {
  defineEvent('PARCEL TYPE SELECTED', {
    event: {
      type: 'PARCEL_TYPE_SELECTED',
      payload: {
        fulfilmentOrder: {
          id: '123abc',
        },
        parcel: {
          heighMm: 1200,
          widthMm: 100,
          depthMm: 3000
        }
      }
    }
  });
  // etc
})

Example: Producer configuration

Producer tests are executed alongside a running instance of the service under test. You could even initialise your service with a test specific runtime in your beforeAll hook.

configureService('serviceA', {
  beforeAll: async () => {
    await setupSeedData();
    await startServer({ eventSpecMode: true });
  },
  beforeEach: async () => {
    await resetSeedData();
  },
  from: {
    serviceB: {
      eventDispatcher: async (event) => {
        await axios.post('/test/webhook/serviceB', event);
        // you could even call your event handling code directly
      },
    },
    serviceC: {
      eventDispatcher: async (event) => {
        await axios.post('/test/webhook/serviceC', event);
      },
    },
  }, 
});

Your server has to run alongside your tests because we are actually going to capture the event publication during a test run. For example:

import { mockPublish } from 'eventspec';

export const publishEvent = async (event: Events) => {
  if (process.env.EVENTSPEC_TEST === 'true') {
    await mockPublish(event);
    return;
  }
  await pubsub.topic('some-topic').publishMessage({ json: event });
}

About


Languages

Language:TypeScript 92.6%Language:JavaScript 7.4%