Understanding Different Types of Automated Tests: Component Tests

I covered unit tests in the previous post of the series. If you haven’t read it, you should start there. Let’s cover component tests now.

What Is a Component Test?

A component test tests a component as a whole without bothering about its internals, including individual units it uses to accomplish its job.

A consequence of not bothering about internals means we also don’t care how a component gets instantiated. That is, we don’t create an instance of a component in the test itself because we would have to pass all its dependencies to it. And since the dependencies themselves might have their own dependencies as well, it would get too ugly to create the whole graph of dependencies for the component under test.

For that reason, we need to use a dependency injection container. A container would assemble the component we want to test along with the whole dependency graph it needs to do its job.

Since component tests are not integration tests, we need to use test doubles to isolate the component under test from other components or external services it integrates with.

Similar to unit tests, component tests can be split up into two groups — leaf component tests and component interaction tests.

Leaf Component Tests

Leaf components do their job and return a result without producing side effects. That’s very similar to leaf units I covered in the corresponding section of the post about unit tests.

Unlike leaf units, leaf components might consist of many individual units each of which does their small job to produce a combined result that a component is responsible for.

Let’s say we have a MarkdownRenderer component that we use to convert Markdown to HTML. Markdown is extensible and there are multiple common extensions used by different systems.

For instance, Markdown has support for code blocks marked with special indentation like 4 spaces or 1 tab. GitHub adds a Markdown extension to support another way to mark a code block by wrapping it between a pair of ```  — fenced code blocks:

```
public static void main(String[] args) {
    System.out.println("Hello, world!");
}
```

We want our MarkdownRenderer to have that extension as well. But we don’t want to leak the fact that it’s an extension to any other part of our system. The only thing the rest of the system needs to know is that our MarkdownRenderer supports fenced code blocks. Whether it’s an extension or not is out of their concern.

Behind the scenes, our MarkdownRenderer could be using a third-party Markdown library with an extension to support fenced code blocks. That extension might come with that third-party library or we might have to write it ourselves.

The main point here is that our MarkdownRenderer will potentially use more than a dozen units of code behind the scenes — whether third-party or our own. And we don’t want to deal with all those units directly. All we care about is the end result we get from the component itself.

Here’s an example:

public class MarkdownRendererTest {
    @Autowired
    private MarkdownRenderer renderer;

    @Test
    public void renderFencedCodeBlock() {
        String input = "";
        input += "```\n";
        input += "public static void main(String[] args) {\n";
        input += "    System.out.println(\"Hello, world!\");\n";
        input += "}\n";
        input += "```\n";

        String output = "";
        output += "<pre><code>";
        output += "public static void main(String[] args) {\n";
        output += "    System.out.println(&quot;Hello, world!&quot;);\n";
        output += "}\n";
        output += "</code></pre>\n";

        assertThat(renderer.render(input), is(output));
    }
}

Our MarkdownRenderer depends on multiple units each doing their job but we just don’t care about it. We inject an instance of it and interact with it using its public API. Whatever happens behind the scenes doesn’t matter to us as long as we get the result we need.

Component Interaction Tests

Even though almost any non-trivial system has at least one leaf component, most components I write tend to interact with other components or external services.

The main reason to interact with a component like that is to get the side effects it produces. A side effect could be changing data in a database or sending an HTTP request to an external API.

Unlike calling a method on a leaf component to get a particular return value, calling a method on an interacting component often doesn’t produce any return value that we can assert.

And since component tests are not integration tests, we don’t want to actually interact with other components or external services.

For these two reasons we need to use test doubles like stubs and mocks to be able to verify that a particular side effect does actually occur.

Since we don’t directly instantiate a component that we want to test, we can’t just pass test doubles to the constructor of the component. That means that test doubles need to be injected into the component by the container itself.

Let’s say we have a SubscriptionManager that manages subscriptions of the users of the system. Now we want to test that creating a subscription for a user results in charging their credit card.

To do its job, the component under test will need to interact with 2 other components and one external service.

The first component will return the price of the plan the user is subscribing to.

The second component will check if there’s a sale in progress and will return the amount that the user needs to be discounted for.

The external service is a payment gateway that we use to charge credit cards.

Here’s an example:

public class SubscriptionManagerTest {
    @Autowired
    private SubscriptionManager subscriptionManager;
    @Autowired
    private FixedSubscriptionPriceResolver priceResolver;
    @Autowired
    private FixedSaleManager saleManager;
    @Autowired
    private PaymentGateway paymentGateway;
    @Autowired
    private Mockery mockery;

    @Test
    public void chargeWhenSubscribing() {
        priceResolver.setPrice(99);
        saleManager.setDiscountAmount(10);

        mockery.checking(new Expectations() {{
            oneOf(paymentGateway).charge(123, 89);
        }});

        User user = aUser()
            .withCreditCardId(123)
            .build();
        subscriptionManager.subscribe(user);

        mockery.assertIsSatisfied();
    }
}

Let’s go through it in detail.

SubscriptionManager is the component we’re testing. It’s a real production class.

FixedSubscriptionPriceResolver and FixedSaleManager are stubbed implementations of SubscriptionPriceResolver and SaleManager interfaces, respectively. Unlike real production implementations of these interfaces, these implementations provide setter methods to set fixed values that will be returned when their real production methods get called.

PaymentGateway is an interface and the container used for tests is injected with a mock of it. That lets us set up expectations for it to be able to verify that SubscriptionManager actually invokes its method.

The reason we went with stubs for SubscriptionPriceResolver and SaleManager but with a mock for PaymentGateway is that the first two return values while PaymentGateway.charge() doesn’t return anything. If we don’t mock it, we can’t verify that it was actually called.

And the reason we went with a mock for PaymentGateway is that this test is not an integration test and hence we don’t want to interact with a real payment gateway. All we need to know is that charge() with proper values has been called. Whether or not a PaymentGateway implementation works properly is for its integration tests to verify.

SubscriptionManager does other things when a user gets subscribed. It creates a subscription record in the database and sends out a confirmation email to the user. But we don’t care about that in this particular test because it’s focused on one and only one thing — charging the credit card the proper amount. There are other tests that deal with other aspects of subscribing a user.

When to Use Component Tests

Component tests are great when you want to test a component as a whole without having to bother with its internal dependencies and interactions with them.

As I mentioned in the first post of the series, when it comes to test automation, most programmers learn about unit tests and don’t go beyond that. But unit tests are too low level and that means spending too much time on both writing and maintaining them.

Unit tests are brittle. There are multiple reasons to it.

First, they instantiate and inject all the dependencies that are needed by a unit. Whenever the unit needs another dependency, every unit test of it needs to be updated.

Second, since unit tests test internal interactions of a unit with its dependencies, refactorings like extracting a class to replace two dependencies with one force us to update the tests as well.

That brittleness coupled with the fact that most programmers only learn about unit tests results in many teams abandoning test automation completely.

Component tests don’t suffer from these problems because they operate on a higher level.

That’s why I personally prefer component tests over unit tests. Component tests give a bigger bang for each buck spent on it.

Stay Tuned

The next post in the series will be about integration tests.

Published by

Elnur Abdurrakhimov

Elnur Abdurrakhimov is a software architect and developer with over a decade of real world experience.