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

  • Writing DTOs With Java8, Lombok, and Java14+
  • Distribution Design Patterns in Java - Data Transfer Object (DTO) And Remote Facade Design Patterns
  • Did You Know the Fastest Way of Serializing a Java Field Is Not Serializing It at All?
  • User-Friendly API Publishing and Testing With Retrofit

Trending

  • The Future of Java and AI: Coding in 2025
  • Rust, WASM, and Edge: Next-Level Performance
  • Event Driven Architecture (EDA) - Optimizer or Complicator
  • Build a Simple REST API Using Python Flask and SQLite (With Tests)
  1. DZone
  2. Coding
  3. Languages
  4. Java Mapper and Model Testing Using eXpectamundo

Java Mapper and Model Testing Using eXpectamundo

By 
Stewart Bissett user avatar
Stewart Bissett
·
Mar. 12, 15 · Interview
Likes (0)
Comment
Save
Tweet
Share
7.8K Views

Join the DZone community and get the full member experience.

Join For Free

As a long time Java application developer working in variety of corporate environments one of the common activities I have to perform is to write mappings to translate one Java model object into another. Regardless of the technology or library I use to write the mapper, the same question comes up. What is the best way to unit test it?

I've been through various approaches, all with a variety of pros and cons related to the amount of time it takes to write what is essentially a pretty simple test. The tendency (I hate to admit) is to skimp on testing all fields and focus on what I deem to be the key fields in order to concentrate on, dare I say it, more interesting areas of the codebase. As any coder knows, this is the road to bugs and the time spent writing the test is repaid many times over in reduced debugging later.

Enter eXpectamundo

eXpectamundo is an open source Java library hosted on github that takes a new approach to testing model objects. It allows the Java developer to write a prototype object which has been set up with expectations. This prototype can then be used to test the actual output in a unit test. The snippet below illustrates the setup of the prototype. 

    ...
    User expected = prototype(User.class);
    expect(expected.getCreateTs()).isWithin(1, TimeUnit.SECONDS, Moments.today());
    expect(expected.getFirstName()).isEqualTo("John");
    expect(expected.getUserId()).isNull();
    expect(expected.getDateOfBirth()).isComparableTo(AUG(9, 1975));
    expectThat(actual).matches(expected);
    ..
For a complete example lets take a simple Data Transfer Object (DTO) which transfers the definition of a new user from a UI.
package org.exparity.expectamundo.sample.mapper;

import java.util.Date;

public class UserDTO {

  private String username, firstName, surname;
  private Date dateOfBirth;

  public UserDTO(String username, String firstName, String surname,
    Date dateOfBirth) {
    this.username = username;
    this.firstName = firstName;
    this.surname = surname;
    this.dateOfBirth = dateOfBirth;
  }

  public String getUsername() {
    return username;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getSurname() {
    return surname;
  }

  public Date getDateOfBirth() {
    return dateOfBirth;
  }
}

This DTO needs to mapped into the domain model User object which can then be manipulated, stored, etc by the service layer. The domain User object is defined as below:

package org.exparity.expectamundo.sample.mapper;

import java.util.Date;

public class User {

  private Integer userId;
  private Date createTs = new Date();
  private String username, firstName, surname;
  private Date dateOfBirth;

  public User(String username, String firstName, String surname,
    final Date dateOfBirth) {
    this.username = username;
    this.firstName = firstName;
    this.surname = surname;
    this.dateOfBirth = dateOfBirth;
  }

  public Integer getUserId() {
    return userId;
  }

  public Date getCreateTs() {
    return createTs;
  }

  public String getUsername() {
    return username;
  }

  public String getFirstName() {
    return firstName;
  }

  public String getSurname() {
    return surname;
  }

  public Date getDateOfBirth() {
    return dateOfBirth;
  }
}

The code for the mapper is simple so we'll use a simple hand coded mapping layer however I've introduced a bug into the mapper which we'll detect later with our unit test.

package org.exparity.expectamundo.sample.mapper;

public class UserDTOToUserMapper {
  public User map(final UserDTO userDTO) {
    return new User(userDTO.getUsername(), userDTO.getSurname(),
      userDTO.getFirstName(),
      userDTO.getDateOfBirth());
  }
}

We then write a unit test for the mapper using eXpectamundo to test the expectation.

package org.exparity.expectamundo.sample.mapper;

import java.util.concurrent.TimeUnit;
import org.junit.Test;
import static org.exparity.dates.en.FluentDate.AUG;
import static org.exparity.expectamundo.Expectamundo.*;
import static org.exparity.hamcrest.date.Moments.now;

public class UserDTOToUserMapperTest {

  @Test
  public void canMapUserDTOToUser() {

    UserDTO dto = new UserDTO("JohnSmith", "John", "Smith", AUG(9, 1975));
    User actual = new UserDTOToUserMapper().map(dto);

    User expected = prototype(User.class);
    expect(expected.getCreateTs()).isWithin(1, TimeUnit.SECONDS, now());
    expect(expected.getFirstName()).isEqualTo("John");
    expect(expected.getSurname()).isEqualTo("Smith");
    expect(expected.getUsername()).isEqualTo("JohnSmith");
    expect(expected.getUserId()).isNull();
    expect(expected.getDateOfBirth()).isSameDay(AUG(9, 1975));
    expectThat(actual).matches(expected);
  }
}

The test shows how simple equality tests can be performed and also introduced some of the specialised tests which can be performed, such as testing for null, or testing the bounds of the create timestamp and performing a comparison check on the dateOfBirth property. Running the unit test reports the failure in the mapper where the firstname and surname properties have been transposed by the mapper.

java.lang.AssertionError: 
Expected a User containing properties :
  getCreateTs() is expected within 1 seconds of Sun Jan 18 13:00:33 GMT 2015
  getFirstName() is equal to John
  getSurname() is equal to Smith
  getUsername() is equal to JohnSmith
  getUserId() is null
  getDateOfBirth() is comparable to Sat Aug 09 00:00:00 BST 1975
But actual is a User containing properties :
  getFirstName() is Smith
  getSurname() is John

A simple fix to the mapper resolves the issue:

package org.exparity.expectamundo.sample.mapper;

public class UserDTOToUserMapper {
  public User map(final UserDTO userDTO) {
    return new User(userDTO.getUsername(),userDTO.getFirstName(),
      userDTO.getSurname(),
      userDTO.getDateOfBirth());
  }
}

But I can do this with hamcrest!

The hamcrest equivalent to this test would follow one of two patterns; a custom implementation of org.hamcrest.Matcher for matching User objects, or a set of inline assertions as per the following example:

package org.exparity.expectamundo.sample.mapper;

import java.util.concurrent.TimeUnit;
import org.junit.Test;
import static org.exparity.dates.en.FluentDate.AUG;
import static org.exparity.hamcrest.date.DateMatchers.within;
import static org.exparity.hamcrest.date.Moments.now;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class UserDTOToUserMapperHamcrestTest {

  @Test
  public void canMapUserDTOToUser() {
    UserDTO dto = new UserDTO("JohnSmith", "John", "Smith", AUG(9, 1975));
    User actual = new UserDTOToUserMapper().map(dto);
    assertThat(actual.getCreateTs(), within(1, TimeUnit.SECONDS, now()));
    assertThat(actual.getFirstName(), equalTo("John"));
    assertThat(actual.getSurname(), equalTo("Smith"));
    assertThat(actual.getUsername(), equalTo("JohnSmith"));
    assertThat(actual.getUserId(), nullValue());
    assertThat(actual.getDateOfBirth(), comparesEqualTo(AUG(9, 1975)));
  }
}

In this example the only difference eXpectamundo offers over hamcrest is a different way of reporting mismatches. eXpectamundo will report all differences between the expected vs the actual whereas the hamcrest test will fail on the first difference. An improvement, but not really a reason to consider alternatives. Where the approach eXpectomundo offers starts to differentiate itself is when testing more complex object collections and graphs.

Collection testing with eXpectamundo

If we move our code forward and we create a repository to allow us to store and retrieve User instances. For the sake of simplicity  I've used a basic HashMap backed repository. The code for the repository is as follows:

package org.exparity.expectamundo.sample.mapper;

import java.util.*;

public class UserRepository {

  private Map userMap = new HashMap<>();

  public List getAll() {
    return new ArrayList<>(userMap.values());
  }

  public void addUser(final User user) {
    this.userMap.put(user.getUsername(), user);
  }

  public User getUserByUsername(final String username) {
    return userMap.get(username);
  }
}

We then write a unit test to confirm the behaviour of repository

package org.exparity.expectamundo.sample.mapper;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.junit.Test;
import static org.exparity.dates.en.FluentDate.AUG;
import static org.exparity.expectamundo.Expectamundo.*;

public class UserRepositoryTest {

  private static String FIRST_NAME = "John";
  private static String SURNAME = "Smith";
  private static String USERNAME = "JohnSmith";
  private static Date DATE_OF_BIRTH = AUG(9, 1975);
  private static User EXPECTED_USER;

  static {
    EXPECTED_USER = prototype(User.class);
    expect(EXPECTED_USER.getCreateTs()).isWithin(1, TimeUnit.SECONDS, new Date());
    expect(EXPECTED_USER.getFirstName()).isEqualTo(FIRST_NAME);
    expect(EXPECTED_USER.getSurname()).isEqualTo(SURNAME);
    expect(EXPECTED_USER.getUsername()).isEqualTo(USERNAME);
    expect(EXPECTED_USER.getUserId()).isNull();
    expect(EXPECTED_USER.getDateOfBirth()).isComparableTo(DATE_OF_BIRTH);
  }

  @Test
  public void canGetAll() {
    User user = new User(USERNAME, FIRST_NAME, SURNAME, DATE_OF_BIRTH);
    UserRepository repos = new UserRepository();
    repos.addUser(user);
    expectThat(repos.getAll()).contains(EXPECTED_USER);
  }

  @Test
  public void canGetByUsername() {
    User user = new User(USERNAME, FIRST_NAME, SURNAME, DATE_OF_BIRTH);
    UserRepository repos = new UserRepository();
    repos.addUser(user);
    expectThat(repos.getUserByUsername(USERNAME)).matches(EXPECTED_USER);
  }
}

The test shows how the prototype, once constructed, can be used to perform a deep verification of an object and, if desired, can be re-used in multiple tests. The equivalent matcher in hamcrest is to write a custom matcher for the User object, or as below with flat objects using a multi matcher. (Note there are a number of ways to write the matcher, the one below I felt was the most terse example).

package org.exparity.expectamundo.sample.mapper;

import java.util.Date;
import java.util.concurrent.TimeUnit;
import org.hamcrest.*;
import org.junit.Test;
import static org.exparity.dates.en.FluentDate.AUG;
import static org.exparity.hamcrest.BeanMatchers.hasProperty;
import static org.exparity.hamcrest.date.DateMatchers.*;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;

public class UserRepositoryHamcrestTest {

  private static String FIRST_NAME = "John";
  private static String SURNAME = "Smith";
  private static String USERNAME = "JohnSmith";
  private static Date DATE_OF_BIRTH = AUG(9, 1975);

  private static final Matcher<user> EXPECTED_USER = Matchers.allOf(
      hasProperty("CreateTs", within(1, TimeUnit.SECONDS, new Date())),
      hasProperty("FirstName", equalTo(FIRST_NAME)),
      hasProperty("Surname", equalTo(SURNAME)),
      hasProperty("Username", equalTo(USERNAME)),
      hasProperty("UserId", nullValue()),
      hasProperty("DateOfBirth", sameDay(DATE_OF_BIRTH)));

  @Test
  public void canGetAll() {
    User user = new User(USERNAME, FIRST_NAME, SURNAME, DATE_OF_BIRTH);
    UserRepository repos = new UserRepository();
    repos.addUser(user);
    assertThat(repos.getAll(), hasItem(EXPECTED_USER));
  }

  @Test
  public void canGetByUsername() {
    User user = new User(USERNAME, FIRST_NAME, SURNAME, DATE_OF_BIRTH);
    UserRepository repos = new UserRepository();
    repos.addUser(user);
    assertThat(repos.getUserByUsername(USERNAME), is(EXPECTED_USER));
  }
}

In comparison this hamcrest-based test matches the eXpectamundo test in compactness but not in type-safety. A type-safe matcher can be created which checks each property individual which would make considerably more code for no benefit over the eXpectamundo equivalent. The error reporting during failures is also clear and intuitive for the eXpectamundo test, less so for the hamcrest-equivalent. (Again an equivalent descriptive test can be written using hamcrest but will require much more code). An example of the error reporting is below where the surname is returned in place of the firstname:

java.lang.AssertionError: 
Expected a list containing a User with properties:
  getCreateTs() is a expected within 1 seconds of Fri Mar 06 17:29:52 GMT 2015
  getFirstName() is equal to John
  getSurname() is equal to Smith
  getUsername() is equal to JohnSmith
  getUserId() is is null
  getDateOfBirth() is is comparable to Sat Aug 09 00:00:00 BST 1975
but actual list contains:
  User containing properties
    getFirstName() is Smith

Summary

In summary eXpectamundo offers a new approach to perform verification of models during testing. It provides a type-safe interface to set expectations making creation of deep model tests, especially in an IDE with auto-complete, particularly simple. Failures are also reported with a clear to understand error trace. Full details of eXpectamundo and the other expectations and features it supports are available on the eXpectamundo page on github. The example code is also available on github.

Try it out

To try eXpectamundo out for yourself include the dependency in your maven pom or other dependency manager

    <dependency>
      <groupId>org.exparity</groupId>
      <artifactId>expectamundo</artifactId>
      <version>0.9.15</version>
      <scope>test</scope>
    </dependency>
unit test Java (programming language) Object (computer science) Data transfer object

Opinions expressed by DZone contributors are their own.

Related

  • Writing DTOs With Java8, Lombok, and Java14+
  • Distribution Design Patterns in Java - Data Transfer Object (DTO) And Remote Facade Design Patterns
  • Did You Know the Fastest Way of Serializing a Java Field Is Not Serializing It at All?
  • User-Friendly API Publishing and Testing With Retrofit

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!