Simplifying Test Developmentoriginally published 2023-03-18
when it comes to tests, i see three conceptual scopes.
business domain - mostly reserved for tests with more integration than individual units of behavior. I don't want to rewrite the code to 'register a new subscription', then 'charge it twice', etc. I want that behavior to be available through a simple method call that is named and parameterized for that model. So I build testing mechanisms that provides these behaviors through an interface based on this conceptual model. In newer code I often end up just dispatching a command, in older code it can be more complex. This is both for behaviors like "charge a subscription" and for assertions like "a subscription was charged".
The domain of our implementation - Similarly to how I don't want to duplicate the domain ideas in tests. I don't want to duplicate the idea of creating a transaction model in a specific state or configurations. So I build testing mechanisms that provide these behaviors. They can provide models, bind fakes to interfaces that interact over boundaries like external api calls, or otherwise. The conceptual domain here isn't the business, it's the domain of our implementation.
The immediate scope I use for unit testing only. I put this input in a function, I expect to get back an object that is correctly wired. This doesn't have the business' conceptual model, it doesn't talk about "registering a subscription". It doesn't have a model about the domain of the implementation, it doesn't talk about "creating a new paid credit card transaction model". It specifically is about the domain of inputs and outputs.
I am not worried about duplicating basic unit input / output validation logic because it's mostly specific to the unit. Sure, I have a component that compares two instances of
SerializablePayload with nice assertion error messages to help the developer see something more useful than "false is not true".
I am very worried about duplicating logic for business domain concepts and concepts related to the domain of our implementation. So I ensure that we build test tooling that reduces redundancy here.
All of this applies to how I feel about mocks as well. It's not that all mocks need to originate in "builders" or "factories". But rather, if I have a mock setup that represents some kind of system behavior or assertion that is on the business domain or implementation domain levels, then I want to create a uniform expectation of it. Naturally, these mechanisms are only useful to service individual contexts.