This image illustrates the contrast between technical motivation, represented by flowing, dynamic data streams, and domain motivation, represented by solid, stable foundational concepts. This visual metaphor illustrates the main point that DTOs facilitate the movement of data between layers, while value objects represent unchangeable domain concepts that form the basis of business logic. The layered architecture also illustrates how these objects are used in different contexts: technical boundaries versus core business logic. It emphasises the stability and predictability that immutability brings to testing practices.
Data Transfer Objects (DTOs) facilitate the flow of data between different layers of a system, whereas Value Objects represent stable concepts within the system's domain.

Building on the distinctions and best practices discussed in my previous article on testing with(out) dependencies, it is important to consider two fundamental object types in the context of testing: Data Transfer Objects (DTOs) and Value Objects.

From technical to domain motivation

DTOs are predominantly technically motivated: they facilitate the movement of data between layers, APIs, or systems, often flattening or reshaping entities for serialisation and transfer. They generally lack domain logic, instead acting merely as containers whose main responsibility is to structure the data rather than enforce behavioural rules. The only logic they should have is that of self-validation. Because, in my opinion, they should validate themselves, just as objects of value do.

By contrast, value objects are motivated by the business domain: they represent fundamental domain concepts such as currency, dates, ranges, and measurements. They are considered equal based purely on their values, not on identity or state transitions.

Immutability reduces cognitive load

As I explained in my previous article, one of the main advantages of immutable objects is that they reduce the cognitive load when reading and understanding code. This is also true for test code: when an object is immutable, there is no need to worry about its internal state changing during testing.

Immutable value objects are created once and never change, thus guaranteeing domain validity. This makes them ideal for testing scenarios where specific values must be asserted or compared. Their predictability enables developers to work with them confidently, safe in the knowledge that there are no hidden side effects.

However, DTOs are often mutable, which is driven by the technical necessity of setting or populating fields from deserialised data sources. When they are mutable, careful setup and ongoing vigilance are required to avoid inadvertent state changes to DTOs across tests.

The mutability of DTOs can and should be avoided. For example, implementing them as immutable containers by using read-only properties offers the benefits of value objects and makes DTO-based test setups less error-prone.

Test code that relies on immutable objects is simpler to both read and write since there is no risk of shared references causing values to change behind the scenes, which is a common source of bugs and confusion.

Test Doubles

In my previous article, I covered the benefits of using properly designed test doubles, stubs and mock objects in detail. These tools can be used to isolate dependencies and clarify intent in tests. Crucially:

Immutable value objects never need stubbing or mocking. They represent fixed data and carry no collaborating dependencies, which makes them the polar opposite of substitutable service or entity dependencies. When a test uses a value object, it is always the real thing, so no test double is necessary.

DTOs generally do not require the use of test doubles either, as they are simple containers. However, if a DTO includes logic or interacts with other components, a test double may be needed to isolate dependent behaviours.

Objects motivated by technical needs (DTOs) tend to be used at boundaries such as serialisation, transport or API interfaces. Objects motivated by domain needs (value objects) express the concepts that form the backbone of business logic. In tests, the stability and simplicity of immutable objects means developers can focus on verifying behaviour without worrying about indirect side effects.

Advice and conclusion

Whenever possible, use immutable objects. Whether in the domain, in the infrastructure, or at the boundaries between these contexts, immutability ensures consistency in tests and eliminates the need for complex test setups or fragile assertions.

Do not replace value objects in your tests with test stubs or mock objects. Instead, use real instances to guarantee correctness. Since value objects only represent a fixed value, there is nothing you need to isolate from the code being tested.

If your framework supports it, consider designing DTOs as immutable. This gives DTOs many of the practical advantages of value objects and bridges the gap.

Testing maintainable, readable code requires a foundation of clear intentions and predictable behaviour. As discussed in the previous article, the development of test doubles in PHPUnit reflects the increasing clarity of software testing practices. Similarly, the judicious use of immutable value objects and carefully designed DTOs eliminates the need for stubbing or mocking such objects almost entirely.

By making technical and domain motivations explicit, and by choosing immutability wherever possible, developers can create robust, understandable tests that are free from the anxiety of hidden state changes.