DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
  1. DZone
  2. Coding
  3. Languages
  4. Unit testing when Value Objects get in the way

Unit testing when Value Objects get in the way

Giorgio Sironi user avatar by
Giorgio Sironi
·
Jan. 26, 12 · Interview
Like (0)
Save
Tweet
Share
11.68K Views

Join the DZone community and get the full member experience.

Join For Free

Tests developed during TDD can be classified into several levels, depending on the size of the object graph they need to work with. End-to-end tests span the whole application graph, while unit tests usually target a single public class at a time.

In the middle we find functional tests, which exercise a group of objects. A recurring problem is that of nearby classes C creeping into unit tests of unrelated classes; this situation transform what would be a unit test of the original class O into a functional tests of O and C together (possibly with multiple C classes involved).

Functional tests are handy for specifying behavior at an higher level of abstraction than that of a single object, and sometimes for checking the wiring of a component of the application. However, if they are introduced involuntarily in place of unit tests they are prone to raise maintenance problems, since they will need to change every time the C class is updated. Moreover, they will fail along with the unit test of C, pointing to a problem into either O or C, which are not able to localize immediately.

Consider this test, where the original class is DocumentsDeclarationNodeCommand and the collaborating one is InMemoryDocumentCopy:

    @Test
    public void shouldSendTheListOfDocumentsAndWaitForAcknowledgement() throws ConnectionClosedException
    {
        UpstreamConnection upstream = mock(UpstreamConnection.class);
        DownstreamConnection downstream = mock(DownstreamConnection.class);
        InMemoryDocumentCopy first = new InMemoryDocumentCopy("1.txt", "hello");
        InMemoryDocumentCopy second = new InMemoryDocumentCopy("2.txt", "hello2");
        DocumentsDeclarationNodeCommand command = DocumentsDeclarationNodeCommand.fromDocumentCopies(
                Arrays.<DocumentCopy>asList(first, second), 10001);
        
        command.execute(upstream, downstream);
        
        InOrder inOrder = inOrder(upstream, downstream);
        inOrder.verify(upstream).command("DOCUMENTS|PORT=10001");
        inOrder.verify(upstream).command("1.txt|5");
        inOrder.verify(upstream).command("2.txt|6");
        inOrder.verify(upstream).endCommandSection();
        inOrder.verify(downstream, times(1)).readResponse();
    }

The two expectations on command() make this test a functional one: a change in the textual serialization format of InMemoryDocumentCopy (such as "1.txt|sha1_hash|5") will break this checks, even if DocumentsDeclarationNodeCommand still works. Yet we cannot avoid to verify that the documents are really sent to the server by this object.

Functional tests can be transformed again into unit tests by testing O with a Test Double instead of C (a Stub, or a Mock.) The only remaining dependency will be the one of the interface of C, which can be even extracted into an independent entity (a first-class interface in language that support them such as Java, C# and PHP.)

Pure functions

What happens when you can't easily inject a Test Double to maintain the tests at the unit level? This issues exists in functional languages where functions call a tree of other functions.

A analogue approach to dependency injection is to inject the function as a parameter, but doesn't probably scale to the level of injection we perform on objects: every function signature would have to receive all the collaborating ones as additional parameters. There are even mocking frameworks for functional languages like Marick's one which are able to isolate a function from its collaborators.

Uncle Bob uses the Derived Expectation pattern instead:

testing "update-all"
  (let [
    o1 (make-object ...)
    o2 (make-object ...)
    o3 (make-object ...)
    os [o1 o2 o3]
    us (update-all os)
    ]
    (is (= (nth us 0) (reposition (accelerate (accumulate-forces os o1)
    (is (= (nth us 1) (reposition (accelerate (accumulate-forces os o2)
    (is (= (nth us 2) (reposition (accelerate (accumulate-forces os o3)
    )
  )

The update-all function calls internally reposition, accelerate and accumulate-forces (or it calls other functions which in turn call these three). Instead of specifying unreadable literal expectations in the tests like (1.096, 4.128), this approach let the test specify update-all link to the other functions without introducing magic numbers. It is therefore a unit test for update-all, while the same test containing numbers would be a functional test.

Note that this approach is safe for functional languages because the collaborating functions have no state, being pure; you can call reposition and accelerate how many times you want, and their result won't change. This is not necessarily true for collaborators in object-oriented languages: in principle, a method can return a different value for each call.

Tests with derived expectations

As long as the composed methods do not change their result, this approach would build real unit tests, whose success does not depend on the correctness of classes other than the one under test. Apart from corner cases like the composed methods throwing exceptions, a change in the collaborator's behavior would change only the collaborator's test.

Value Objects are the ideal collaborator to stub out with derived expectations:

  • they are immutable, so their methods always return the same result.
  • Their code is usually self-contained and simple, so it's difficult for a method to throw an exception or to break internally once the Value Object has been correctly built.
  • Being simple, final classes they do not implement an explicit interface; and they are not commonly substituted by Test Doubles. Their behavior is mixed in with the objects using them.
The test becomes:
   @Test
    public void shouldSendTheListOfDocumentsAndWaitForAcknowledgement() throws ConnectionClosedException
    {
        UpstreamConnection upstream = mock(UpstreamConnection.class);
        DownstreamConnection downstream = mock(DownstreamConnection.class);
        InMemoryDocumentCopy first = new InMemoryDocumentCopy("1.txt", "hello");
        InMemoryDocumentCopy second = new InMemoryDocumentCopy("2.txt", "hello2");
        DocumentsDeclarationNodeCommand command = DocumentsDeclarationNodeCommand.fromDocumentCopies(
                Arrays.<DocumentCopy>asList(first, second), 10001);
        
        command.execute(upstream, downstream);
        
        InOrder inOrder = inOrder(upstream, downstream);
        inOrder.verify(upstream).command("DOCUMENTS|PORT=10001");
        inOrder.verify(upstream).command(first.toString());
        inOrder.verify(upstream).command(second.toString());
        inOrder.verify(upstream).endCommandSection();
        inOrder.verify(downstream, times(1)).readResponse();
    }

Conclusion

We saw that Test Doubles like Mocks and Stubs are not the only way to achieve isolated tests, which fail only where the class under test fail and not when a collaborator changes its implementation.

In the Example, DocumentsDeclarationNodeCommand is tested by involving the real collaborator, but setting up Derived Expectation from it instead of literal ones. The result is this test is only tied to the method signatures of the collaborator instead of to the real behavior (the output format of toString()).

This technique doesn't need to be used often: its purpose is to isolate from an immutable object, without introducing a Test Double.

unit test Object (computer science)

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • When to Choose Redpanda Instead of Apache Kafka
  • Java REST API Frameworks
  • Reliability Is Slowing You Down
  • Container Security: Don't Let Your Guard Down

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: