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.

img_1.png img_1.png

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.

img_2.png img_2.png

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.

img_1.png img_1.png

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.