Implements several simulators for common dependencies. These abstractions make it easier to write highly-testable code.
View the Project on GitHub arlobelshee/SimulatableApi
License: MIT.
Your program needs to run on several platforms or it depends on slow components (databases, files, networks, or users).
The first part of the solution is to use a Hexagonal Architecture. In this architecture, your program has 4 parts:
The next step is to define a set of test cases that operate on the ports. These ensure that each adaptor for a given port behaves exactly the same—to whatever degree your application wants uniformity.
All of your system other than the adaptors and ports is now platform independent. It can be fully unit tested. The few parts of your system that depend on a port can use the simplest adaptor that will possibly work. Likely, this is an in-memory simulator or a memory-based caching adaptor. So even your integration tests will run quickly.
Platform testing is simple. You run your port test suite against on each target platform, using the adaptor for that platform. Because you run the same tests everywhere, you know that your adaptors are ensuring consistency between your platforms.
The remaining work is to continually ensure that your port tests include all behavior that your application depends on. When you need more out of a port you update the suite for that port to include the new behavior. You then extend all the adaptors to pass the new tests.
As I've used this architecture and testing pattern on several projects, I've found certain ports come up over and over. I finally got around to extracting them so that I could share them between projects. If you find similar patterns, I invite you to use my simulators.
Note: different applications require different types of uniformity in their ports. So some of these implementations may not work for your project. I think they're a good starting point. These ports have proven to be good abstractions in numerous projects. However, I do expect that you'll modify them some to fit each particular project.
Simulatable API binaries are available on NuGet for those who can use the port abstractions as-is. I've released the source for those who need to modify the abstractions to fit their domains.
My first release contains only one port, and only two adaptors. I will add more ports and adpators over time. Here is the list of planned ports, with a description of each. I'll add full documentation for each as I add it to the library.
This port represents a transactional view on an arbitrary storage medium.
The port's core class is FileSystem
.
There are three ways to get a FileSystem
. The first two, FileSystem.Real()
and FileSystem.Simulated()
, return a new view wrapped around some storage.
The last, FileSystem.Clone()
, creates a view from another view. The two views share the same underlying storage but each has its own change tracking and undo facilities.
This is not your typical files and directories API.
There is a single instance that represents the entire file system; file and directory objects are bound to a file system.
All the instances related to a single file system share consistent state. If you create two file objects pointing to the same path in a particular file system and then write to one, you will immediately see the changes in the second.
Clones are guaranteed to share consistent state. They only difference is where their last savepoint is.
No promise is made about consistency between multiple FileSystem
s unless they are clones of each other. For example, the in-memory adaptor has disjoint state between FileSystem
instances. The on disk adaptor shares state between separate FileSystem
instances.
For this reason, I recommend that you only create one adaptor instance. If you need multiple views then clone your instance. Do not create a new, unrelated instance.