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
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

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
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

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • Chaos Mesh — A Solution for System Resiliency on Kubernetes
  • Clean Unit Testing
  • The Anatomy of Good Unit Testing

Trending

  • Understanding the Shift: Why Companies Are Migrating From MongoDB to Aerospike Database?
  • Supervised Fine-Tuning (SFT) on VLMs: From Pre-trained Checkpoints To Tuned Models
  • The Role of AI in Identity and Access Management for Organizations
  • Exploring Intercooler.js: Simplify AJAX With HTML Attributes
  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

By 
Giorgio Sironi user avatar
Giorgio Sironi
·
Jan. 26, 12 · Interview
Likes (0)
Comment
Save
Tweet
Share
12.6K 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.

Related

  • TDD Typescript NestJS API Layers with Jest Part 1: Controller Unit Test
  • Chaos Mesh — A Solution for System Resiliency on Kubernetes
  • Clean Unit Testing
  • The Anatomy of Good Unit Testing

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

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

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!