---
title: 'Layer Tests'
source: 'https://academia.sh/en/courses/enterprise-application-framework/layer-tests'
course: 'The Enterprise Application Framework'
language: en
updated: '2026-09-13T05:08:56+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Compares, by running it, how many components a slice test and a full context test build when passing through the same container'
    - 'Shows that a setup annotation (`@Setup`) runs only on a component built through the IoC container, and never runs on the same class built by hand'
    - "Measures that the slice test's result never changes even when a component left outside the slice never has its annotation applied"
---

# Layer Tests

The difference between a slice test and a full context test is not an adjective but a number; the same container passing through two setups is placed side by side by counting how many components each one builds. The limiting measurement shows that even when a component's setup annotation left outside the slice never runs at all, the slice test's result never changes, and the test still passes.

The previous lesson measured the claims a token carries and the source of validation, through a
single call. This lesson steps back and enlarges the question: how many components does a
container actually bring up, and which of these does a given test actually build? Two test
setups are modeled — a slice test and a full context test — and the difference between them is
measured not by an adjective but by a number: the number of components the container builds. How
many components does the slice test require, how many does the full context test build, and
does the slice test notice when something outside the slice breaks? The scope and purpose of
layer tests were built in the Integration, Contract and End-to-End Testing and the Build,
Testing and the Ecosystem courses and are not repeated here; this lesson only shows, by number,
how different the two setups are from the container's point of view.

The word "layer" here points to the layers in the Web and Data Layer topic — controller,
service, data access. A slice test usually targets only one of these layers and replaces the
layer above or below it with a fake implementation; this lesson shows how that substitution is
done through the IoC container and how the result can be counted.

## Placing Two Setups Side by Side: Component Count

The modeled application is built from four parts: a data store interface and its real
implementation, a service interface and its real implementation, a controller depending on that
service, and an independent logger with no relation to the service at all. The same `Container`
class is the same mechanism as the IoC container built in the Core Concepts topic — it resolves
its constructor dependencies by reflection, calling `@Setup`-marked methods after construction —
here only the part used in this lesson is rewritten.

```java
// Setup.java — annotation type marking a method to be called after construction (helper source, no main)
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD)
@interface Setup { }
```

```java
// Container.java — small IoC container that resolves its constructor dependencies by reflection and tracks the count of every component it builds (helper source, no main)
import java.lang.reflect.*;
import java.util.*;

class Container {
    final Map<Class<?>, Class<?>> bindings = new HashMap<>();
    final List<Object> built = new ArrayList<>();

    Container bind(Class<?> iface, Class<?> impl) { bindings.put(iface, impl); return this; }

    @SuppressWarnings("unchecked")
    <T> T build(Class<T> type) throws Exception {
        Class<?> real = type.isInterface() ? bindings.get(type) : type;
        Constructor<?> ctor = real.getDeclaredConstructors()[0];
        Class<?>[] paramTypes = ctor.getParameterTypes();
        Object[] arguments = new Object[paramTypes.length];
        for (int i = 0; i < arguments.length; i++) arguments[i] = build(paramTypes[i]);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(arguments);
        for (Method m : real.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Setup.class)) { m.setAccessible(true); m.invoke(instance); }
        }
        built.add(instance);
        return (T) instance;
    }

    static void show(String label, Object value) {
        System.out.printf("%-52s: %s%n", label, value);
    }
}
```

The application's four components sit in separate files. `RealService` carries a setup hook —
the `status` field starts at `not-ready`, and turns to `ready` once the `@Setup`-marked
`prepare` method runs. `FakeService`, by contrast, is the fake the slice test holds in place of
the real service — it carries no setup hook at all, because there is no need for one. Its
constructor is also empty: `RealService` requires a `DataStore`; `FakeService` requires nothing.
This difference also shrinks the slice test's component count — building `FakeService` never
triggers `InMemoryStore` at all, because there is no dependency to resolve.

```java
// DataStore.java — the interface for data access (helper source, no main)
interface DataStore { String read(); }
```

```java
// InMemoryStore.java — the real implementation of the data store (helper source, no main)
class InMemoryStore implements DataStore {
    public String read() { return "data"; }
}
```

```java
// Service.java — the interface for the business logic (helper source, no main)
interface Service { String process(); }
```

```java
// RealService.java — the real implementation of the service; its status stays 'not-ready' until the setup hook runs (helper source, no main)
class RealService implements Service {
    final DataStore store;
    String status = "not-ready";

    RealService(DataStore store) { this.store = store; }

    @Setup void prepare() { status = "ready"; }

    public String process() { return status + ":" + store.read(); }
}
```

```java
// FakeService.java — the fake implementation held by the slice test, carrying no setup hook at all (helper source, no main)
class FakeService implements Service {
    public String process() { return "fake-data"; }
}
```

```java
// Controller.java — depends on the service, the slice test's actual target (helper source, no main)
class Controller {
    final Service service;
    Controller(Service service) { this.service = service; }
    String respond() { return service.process(); }
}
```

```java
// Logger.java — has no dependency on the service at all, an independent part of the full context (helper source, no main)
class Logger {
    String status() { return "ready"; }
}
```

Two separate containers are built: `sliceContainer` binds `Service` to `FakeService`;
`fullContainer` binds the same interface to `RealService`. Both ask for the same `Controller`,
both call the same `build` method — the only difference is in the binding table.

```java
// ComponentCount.java — with the same Controller, how many components are built when requested from two different containers
public class ComponentCount {
    public static void main(String[] args) throws Exception {
        Container sliceContainer = new Container()
                .bind(Service.class, FakeService.class)
                .bind(DataStore.class, InMemoryStore.class);
        Controller sliceController = sliceContainer.build(Controller.class);

        Container fullContainer = new Container()
                .bind(Service.class, RealService.class)
                .bind(DataStore.class, InMemoryStore.class);
        Controller fullController = fullContainer.build(Controller.class);
        fullContainer.build(Logger.class);

        Container.show("slice test, number of components built", sliceContainer.built.size());
        Container.show("full context test, number of components built", fullContainer.built.size());
        Container.show("slice test, Controller.respond()", sliceController.respond());
        Container.show("full context test, Controller.respond()", fullController.respond());
    }
}
```

```
slice test, number of components built              : 2
full context test, number of components built       : 4
slice test, Controller.respond()                    : fake-data
full context test, Controller.respond()             : ready:data
```

**ST12.** Both the slice test and the full context test pass through the same `Container`
class; the difference between them is measured not in the structure of the code but in the
number of components the container builds. **ST13.** The slice test even builds the direct
dependency of the `Controller` it tests through the IoC container, using a fake implementation
— `FakeService` also goes into the `built` list — but the total component count is still lower
than the full context test's: two rather than four. **ST14.** The full context test builds the
entire dependency chain (`RealService`, `InMemoryStore`) as well as independent components that
never enter the chain at all (`Logger`); even a component the `Controller` has no need for
becomes part of the full context, because the full context test brings up the entire
application.

The `build` method itself does not know at all which test is behind which setup; all it knows
is the binding table and the constructor signatures. The same method, on seeing a `Service`
parameter, looks at the `bindings` map and builds whatever it finds there — `FakeService` for
`sliceContainer`, `RealService` for `fullContainer`. The difference is in the test's intent, not
in the code itself. This is also why the component count matters: the slice test runs fast
because it builds few components, since every component built by reflection carries a
constructor resolution and, if needed, a setup hook call; the full context test does all of
this, and a component count that quadruples means roughly a quadrupled build cost.

## An Annotation Left Outside the Slice

`RealService`'s setup hook runs only when the IoC container builds it. If the same class is
constructed by hand with `new`, the `@Setup`-marked `prepare` method never runs at all — this
is the same limiting reading built in the Core Concepts topic, measured again here on a service
class.

```java
// OutsideTheSlice.java — RealService's setup hook never runs at all outside the container; the slice test is entirely independent of it
public class OutsideTheSlice {
    public static void main(String[] args) throws Exception {
        Container sliceContainer = new Container()
                .bind(Service.class, FakeService.class)
                .bind(DataStore.class, InMemoryStore.class);
        Controller sliceController = sliceContainer.build(Controller.class);

        Container fullContainer = new Container()
                .bind(Service.class, RealService.class)
                .bind(DataStore.class, InMemoryStore.class);

        RealService manuallyBuilt = new RealService(new InMemoryStore());
        Service builtByContainer = fullContainer.build(Service.class);

        Container.show("RealService, manually built, @Setup never ran", manuallyBuilt.process());
        Container.show("RealService, built via container, @Setup ran", builtByContainer.process());
        Container.show("slice test, Controller.respond() (independent of RealService)", sliceController.respond());
    }
}
```

```
RealService, manually built, @Setup never ran       : not-ready:data
RealService, built via container, @Setup ran        : ready:data
slice test, Controller.respond() (independent of RealService): fake-data
```

The first two lines show two different ways of constructing the same class: built by hand it is
`not-ready:data`, built through the container it is `ready:data` — the annotation applies only
to the second. **ST15.** A setup annotation is applied only when the IoC container builds it;
the same class built by hand never sees the annotation at all, because the party that reads and
invokes the annotation is the container itself. The third line is the lesson's real point: the
slice test's result — `fake-data` — is entirely independent of whether `RealService` was built
by hand or through the container, because the slice test never sees `RealService` at all. Even
if `RealService`'s setup hook broke, was removed, or the class itself was deleted entirely, the
slice test would never know.

**ST16.** Even when a component left outside the slice never has its annotation applied, the
slice test's result never changes, and the test still passes — this is the lesson's limiting
measurement. The slice test validates whether `Controller` calls `Service` correctly; it never
validates whether `RealService`'s setup hook works at all, because that lies outside its
verification surface. Building fewer components means verifying less — the slice test is fast
and focused, but every annotation, every setup hook belonging to `RealService` sits entirely
outside the slice's field of view. The common definition's defect classification covers this
case exactly: from the slice test's point of view, `RealService`'s broken setup hook is neither
an `error` nor `silent` — it is entirely **ineffective** from the test's own perspective,
because the test has no awareness that the hook even exists. The only thing that closes this
gap is the full context test, because only that setup actually brings `RealService` up and can
check whether its hook ran.

The two test types do not substitute for one another; they answer different questions:

| Question | Slice test | Full context test |
|---|---|---|
| Number of components built | 2 | 4 |
| Is it verified that `Controller` calls `Service` correctly | yes | yes |
| Is `RealService`'s setup hook verified | no | yes |
| Is `Logger`'s existence verified | no | yes |

The table comes down to a single conclusion: the slice test's cheapness comes with a gap, and
this gap is not random — every component the container never built in that test sits outside
the slice's field of view. If a codebase is protected only by slice tests, the real connection
between components is never actually exercised by running it; only the full context test, at a
cost proportional to the number of components it builds, can close that gap.

In practice a codebase keeps both, and not for the same purpose: slice tests run often on every
change, because building few components makes them fast, and they quickly validate the behavior
of a single layer such as `Controller`; full context tests run less often, because building more
components makes them slower, but in exchange they test the real connection between components.
The number measured in this lesson — two in the slice, four in the full context — is on a small
example; in a real application this ratio can range from the tens into the hundreds, and as the
gap grows, so does the surface the slice test never validates.

## Summary

- The slice test and the full context test pass through the same container; the difference
  between them is measured by the number of components the container builds.
- The slice test can build even the direct dependency of the class it tests with a fake
  implementation; even so, the total component count stays lower than the full context test's.
- The full context test builds the entire dependency chain as well as independent components
  that never enter the chain at all.
- A setup annotation is applied only when the IoC container builds it; the same class built by
  hand never sees the annotation at all.
- Even when a component left outside the slice never has its annotation run, the slice test's
  result never changes, and the test still passes; building fewer components means verifying
  less.

## Next Step

This lesson counted which components were built; the next lesson asks about something never
built, never written at all. There are endpoints that ask from the outside whether an
application is in working order, and most of these endpoints never appear in the source at all
— no class, no method, no annotation. The next lesson measures how many of these endpoints
exist without ever being written, and which information gets given out to the outside.
