A Producer sends messages that others consume. A Consumer consumes that message. To ensure that changes in either don't break the overall system, we can enforce a contract. The Producer provides a guarantee of what it produces. The Consumer publishes an expectation of what it needs.
The guarantees and expectations can be published in a centralized store such that both sides can check that they aren't breaking anything.
This has example code of one producer, ItemPriceUpdater
that sends messages on a Rabbit exchange about the change in price of
an item. There are two consumers, PriceCacheHandler
, which updates a cache of item prices for financial purposes, and
PackSlipHandler
, which updates packing slips with the updated item prices.
schemas/
- schemas for the producers and consumers
item_price_change.schema.json
- The schema of what the producer is sendingpack_slip_new_price.schema.json
- The schema of what the PackSlipHandler
is expectingprice_cache_price_change.schema.json
- The schema of what the PriceCacheHandler
is expectingcentral_authority/
- mimics a central server where the guarantees and expectations are stored
expectations
- mimics the expectations consumers have on producersfinancial_data_warehouse.cache_price.price_change.expectation.json
- An app called "financial data warehouse" has an
expectation on the "price change" guarantee related to a use case called "cache price"wms.pack_slip_does_not_exist.price_change.expectation.json
- An app called "wms" has an expectation
on the "price change" guarantee related to a use case called "pack slip does not exist"wms.pack_slip_exists.price_change.expectation.json
- An app called "wms" has an expectation on the
"price change" guarantee related to a use case called "pack slip exists"guarantees
price_change.guarantee.json
- a guaratee called "price change" that a message will be sent and others can rely onWhen you run the test of the producer, it requires a schema for its payload, and writes out the guarantee. When you run the test of a consumer, it grabs that guarantee and feeds an example payload into itself to start the test. It evaluates the payload against it's schema and then writes out an expectation capturing all this. The producer, when the tests are re-run, will grab these expectations and evaluate the sample payloads against it's schema.
A guarantee consists of three parts:
An expectation consists of three parts:
The system requires three components to work:
Absent this system, the producer would have a test that asserts it sends a message. The testing framework here would augment that test to produce a guarantee. It must:
It must further, if given an Expectation:
it "sends a message" do
allow(Pwwka::Transmitter).to receive(:send_message!)
MyApp.do_stuff!
expect(Pwwka::Transmitter).to have_received(:send_message!).with(payload)
guarantee = Guarantee.new(schema: "my_schema.json", id: "My Stuff", metadata: { routing_key: "sf.foo.bar" })
expect(Pwwka::Transmitter).to have_received(:send_message!).with(provides_guarantee(guarantee))
end
Absent this system, the consumer would have a test that when it receives a message, that message triggers some expected code. The testing framework here would:
Further, it must:
it "processes a message" do
end