Principle 3: Test at Facade level
Now what will happen if we stretch our rule 2 to the maximum? What if we could cover the whole system with tests that are not coupled to any implementation details?
Test at the highest abstraction level
How about we connect Principle 3: be sociable
and test our system at the highest abstraction level possible?
This way we could maximize the stability of our tests and minimize the risk of them going đ´ due to implementation changes.
What is the highest abstraction level?
The highest abstraction level is the one that is used by other modules, teams or end customers to communicate with your system.
In code it will be used by Kafka consumer, a REST client or a different module in your system.
And there is one pattern that we could use to make it more intuitive - Facade
pattern.
Facade pattern
Facade
pattern is a design pattern to provide a simple API (most probably an interface) that hides details of functionality from the consumer of the API.
Hereâs a simple showcase of what Facade
is example in a Shopping Cart Unit
:
public interface ShoppingCartFacade {
CartCreatedResponse createCart();
void addToCart(CartId cartId, Item item, int quantity);
void removeFromCart(CartId cartId, Item item, int quantity); // cartId is retrieved from `addToCart` response
void updateQuantity(CartId cartId, Item item, int quantity);
void clearCart(CartId cartId);
void applyPromoCode(CartId cartId, String promoCode);
}
In code, itâll be an interface that exposes all public functionalities of your unit.
It is a great way to hide implementation details, and you can use it directly in tests. From now on weâll use the Facade
pattern to describe the highest level of abstraction in your module under test.
(yes, donât be afraid of using âfacadeâ word in your code - youâre allowed to use design patterns names to better describe your intent to your fellow team! We encourage you to do that!)
Using Facade in tests
Hereâs an example of test that uses a Facade
as a Black Box
pattern to test addToCart
functionality:
@Test
public void addingItemToCart() {
//given a new cart and a new Item
ShoppingCartFacade shoppingCartFacade = ShoppingCartFacade.build();
Item item = createItem();
CartCreatedResponse cartCreatedResponse = shoppingCartFacade.createCart();
//when an item is added to the cart
shoppingCartFacade.addToCart(cartCreatedResponse.cartId(), item, 1);
//then item should be in the cart
Cart cart = shoppingCartFacade.getCart(cartCreatedResponse.cartId());
Assertions.assertEquals(List.of(item), cart.getItems());
}
In the example above we connect all 3 principles in a single test. Weâre using a Facade
as a black box to test the behaviour of the addToCart
functionality in a full sociable test.
Weâre hiding some details in this example (for example how facade is created), weâll talk more about another time. [//]: # (ÂŤhow to create facadeÂť)
Hexagonal architecture and Facade
If youâre familiar with the Hexagonal architecture - it goes naturally with Facade
pattern. (if youâre not familiar with it - you can learn more about this pattern from an awesome blogpost here).
In Hexagonal architecture you can think of the Facade
as an interface representing âapplication coreâ functionalities, whereas Rest Controller and Kafka consumer are Primary Adapters.
what if my system doesnât have a facade-like interface?
It might be that your current system due to its design doesnât have a Facade
-like interface that you could use in tests.
In that case you can try to extract one gradually and cover that part with tests. If youâre adding a new requirement (consuming new kafka topic, adding new REST endpoint) you can start with creating a Facade
as the entry point to your domain.
You can also extract specific behaviour into facade to enable resilient tests during some refactoring or modification of existing functionalities.
Final thoughts
Use a Facade
pattern to test your system at the highest abstraction level possible.
It will make your tests more stable and less prone to changes when your implementation details change and automatically works great with previously mentioned principles.
itâs not possible to test at the highest abstraction level
After reading all this you might be thinking âyou cannot test everything in this manner, that simply wonât workâ. And thatâs partially true - not everything should be tested at the highest abstraction level.
But focusing on that first, and then adding small function/class level tests will make sure all the corner cases are covered as well. But the core of your system behaviour will always be covered by those high-level tests, and your core - which is often the most important for the end-customers and doesnât change that often - will be stable and well-tested.