Compartilhar via


Port/Adapter/Simulator: read-only and write-only dependencies…

When dealing with many external dependencies, the Port/Adapter/Simulator pattern works great.

But what about dependencies that are read-only – such as consuming an information feed from another system – or write-only – such as sending a notification to a user? Can we use P/A/S in those scenarios?

The answer is a qualified "yes" – we can get many of the advantages of P/A/S, though we will probably have to give up something.

Stock prices

Let's consider an application where we deal with stock prices. If we were in a read/write scenario, we would write something like this:

interface IStockPriceStore
{
    Decimal Load(StockSymbol stockSymbol);
    void Save(StockSymbol stockSymbol, Decimal price);
}

And the test that we would write would look something like this:

IStockPriceStore stockPriceStore = new StockPriceStoreMemory();
stockPriceStore.Save("MSFT", 55m);

StockProcessor stockProcessor = new StockProcessor(stockPriceStore);
stockProcessor.Process();
Assert(…);

Okay, that's not perhaps as minimal as I would like for a test, but it's not terrible.

But what if our stock feed is read-only? That changes our adapter to be something like:

interface IStockPriceSource
{
    Decimal Load(StockSymbol stockSymbol);
}

To make our tests run the same way, we need someplace to put a Save() method. My approach is to put it directly on the simulator (StockPriceSourceMemory) class. There are a few ways I've played around with expressing this:

  1. Put it directly on the class and name it Save()
  2. Put it directly on the class and name it SimulateSave()
  3. Define a separate interface (IStockPriceSink?), put it there, an have the simulator implement it.

 

I don't really like the third option, as we have an interface that will only have one implementation. I haven't decided between the first two; sometimes I like the division that option #2 gives, sometimes I think it's too wordy.

This approach lets us write the product tests exactly the same way we would with a read/write store, but we lose a big advantage of P/A/S – the ability to test for similarity between our real and in-memory implementations of the adapter.

In many cases, I don't think this matters a lot, as read-only stores tend to be fairly straightforward. In some cases, it does matter, and there's another technique we can use. We write our adapter tests so that they pass against the real adapter, and then, when we run those same tests against the simulator, we pre-populate it by fetching specific data from the real adapter and storing it into the simulator. The data that we move across is enough to verify that the simulator works correctly.

This will result in two different sets of tests; there will be the pure-simulator unit tests, and then the contract verification tests that we run against the live data and the simulator.

It's not a perfect solution, but it will be pretty close.

Email User

Emailing a user is a good example of a write-only operation. We'll write the port like this:

interface IUserNotifier
{
    void Notify(User user, Message message);
}

I'm really hoping that you weren't expecting a IEmailUser port.

In this case, our simulator needs to do a couple of things. It needs to verify that Notify was called with appropriate parameters, and it needs to be able to simulate any error situations that we want to raise with the calling application.

How about something like this:

class UserNotifierSimulator: IUserNotifier
{
    void Notify(User user, Message message) {}

    User User {get; private set;}
    Message Message {get; private set;}
}

To use the simulator, I pass it into a class that needs to perform a notification and then look and the User and Message properties to see whether it performed the notification.

As for the port tests, I suspect that I'm going to write a few around the port validation behavior to define what happens when either user or message is improperly formed.

There are other possible errors – the network could be unreachable, for example – but I'm not sure I'm going to write any of them into the port. I would prefer to hide them underneath, to make the real adapter responsible for recovery.

Message passing architectures…

And, so that's how I've done it, and it's worked pretty well.

But… remember my earlier comment about the test code I wrote for the stock store? The problem there is that I'm saving something into a store just so that the stock processor can fetch it right out. But there's an alternative:

If I think of this as in message-passing/pipeline terms, I can express things differently. Something like:

class StockPriceSource
{
    public event EventHandler<StockPrice> StockPriceReady;

    public void FetchStockPrice(StockSymbol stockSymbol);
}

I can then hook this up to a modified version of the StockProcessor class by just hooking a method to an event. This approach means that I don't really need my simulator any more; I have something that can produce one (or a set of) stock prices, and then things that know how to consume them.

I think this probably makes my world simpler. I can write a few quick contract tests for StockPriceSource to verify that it is working, and I can test all the processing just by passing in data.