Spring Boot + JPA — Clear Tests
Tags: java, testing, tutorial, spring
Published: 2021-12-16 16:54:30

I have written several articles about testing Spring Boot applications. We discussed [integration testing with database](https://dev.to/kirekov/spring-boot-testing-data-and-services-288f) and [Testcontainers library](https://dev.to/kirekov/spring-boot-testing-testcontainers-and-flyway-2jpd) particularly. But today I want to cover an even more important topic. You see, tests are essential to building robust software. But poorly written tests are not the case. They can slow down the development process and make code hard to change and maintain.

When it comes to testing, one has to declare test data. It can be items in the `ArrayList`, records in the database, or messages in the distributed queue. The behaviour cannot be tested without data.

So, today we're discussing the patterns of creating test rows in the database with Spring Boot + Spring Data + JPA usage.

You can find all code snippets in the [repository](https://github.com/SimonHarmonicMinor/spring-boot-testing-data-patterns).

## Domain Model

Assuming we're are developing the [dev.to](https://dev.to/) like service. People can make posts and leave comments. Here is the database schema.

![database schema](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6vkogaraegcastqrdmhm.png)
 
And here are the corresponding Hibernate entities.

### Post

```java
@Entity
@Table(name = "post")
public class Post {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  private String name;

  private String content;

  private double rating;

  @OneToMany(fetch = LAZY, mappedBy = "post", cascade = {PERSIST, MERGE})
  private List<Comment> comments;

  @ManyToOne(fetch = LAZY)
  private User author;

  private OffsetDateTime createdAt;
}
```

### Comment

```java
@Entity
@Table(name = "comment")
public class Comment {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  @ManyToOne(fetch = LAZY)
  private User author;

  private String text;

  private OffsetDateTime createdAt;

  @ManyToOne(fetch = LAZY)
  private Post post;
  // getters, setters
}
```

### User

```java
@Entity
@Table(name = "user_table")
public class User {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;

  private String login;

  private String firstName;

  private String lastName;
  // getters, setters
}
```

## Business Case

Suppose we should display top N posts sorted by rating in descending order with comments.

```java
public interface CustomPostRepository {
  List<PostView> findTopPosts(int maxPostsCount);
}

public record PostView(
    Long id,
    String name,
    double rating,
    String userLogin,
    List<CommentView> comments
) {
}
```

> The implementation details have been omitted for brevity because it's not the topic of the article. You can find the complete working solution in the [repository](https://github.com/SimonHarmonicMinor/spring-boot-testing-data-patterns).

Time to write some tests. Suppose we have 10 posts. Each post has 2 comments. And we want to retrieve the top 3 posts. That's how the test may look like.

```java
@SpringBootTest
class PostRepositoryTest extends AbstractIntegrationTest {
  @Autowired
  private PostRepository postRepository;
  @Autowired
  private CommentRepository commentRepository;
  @Autowired
  private UserRepository userRepository;

  @Test
  @DisplayName("Should return top 3 posts")
  void shouldReturnTop3Posts() {
    final var user = userRepository.save(new User("login", "", ""));
    for (int i = 0; i < 10; i++) {
      final var post = postRepository.save(
          new Post("name" + i, "content", i, user, OffsetDateTime.now())
      );
      commentRepository.saveAll(
          List.of(
              new Comment(user, "comment1", OffsetDateTime.now(), post),
              new Comment(user, "comment2", OffsetDateTime.now(), post)
          )
      );
    }

    final var res = postRepository.findTopPosts(3);

    assertEquals(3, res.size(), "Unexpected posts count");
    assertEquals(
        res,
        res.stream()
            .sorted(comparing(PostView::rating, reverseOrder()))
            .collect(Collectors.toList()),
        "Posts should be sorted in by rating in descending order"
    );
    assertTrue(
        res.stream()
            .allMatch(post -> post.comments().size() == 2),
        "Each post should have 2 comments"
    );
  }
}
```

![test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/quqoze8spy8bz4sowdmg.png)

This test passes. It works correctly. Anyway, some problems should be mentioned.

1. The `"given"` phase is way too complex
1. The test has too many dependencies
1. Assertions are vague and unclear

Before performing the query we need to insert some records into the database. In this case, entities are being instantiated via constructors. Seems like a good design choice. All the required attributes can be passed as parameters. If another field appears later, it can be included in the constructor as well. So, we can guarantee that all `non-null` fields are present.

Well, the devil is in the details.

1. The test code becomes too coupled with the constructor definition. You see, there can be plenty of tests that need to create posts, comments, and users. If any of these entities' constructors get enhanced with another attribute, all parts of the code that instantiate objects directly won't compile.

1. Not all the entities' fields are necessary in every test case. For example, while testing the `findTopPosts` method we're only interested in the `rating` value of the `Post` entity.

1. The arguments are not self-explanatory. `Post` entity constructor has 5 positional arguments. Every time you look through the test suite you have to stop and analyze the target values that will be assigned.

1. The test case is _imperative_ rather than _declarative_. Why is that a problem? Because tests are the API contracts. Well written tests act as perfect class documentation.

We can come around with [Object Mother Pattern](https://martinfowler.com/bliki/ObjectMother.html). A simple factory with multiple creating policies. That is how we can instantiate new `Post` objects. 

```java
public class PostTestFactory {
  public static Post createPost(double rating, User author) {
    final var post = new Post();
    post.setName("");
    post.setContent("");
    post.setRating(rating);
    post.setAuthor(author);
    post.setCreatedAt(OffsetDateTime.now());
    return post;
  }
}
```

Similar factories can be created for all entities. So, now the test looks like this.

```java
@Test
@DisplayName("Should return top 3 posts")
void shouldReturnTop3Posts() {
  final var user = userRepository.save(createUser("login"));
  for (int i = 0; i < 10; i++) {
    final var post = postRepository.save(
        createPost(i, user)
    );
    commentRepository.saveAll(
        List.of(
            createComment(user, post),
            createComment(user, post)
        )
    );
  }

  final var res = postRepository.findTopPosts(3);

  assertEquals(3, res.size(), "Unexpected posts count");
  assertEquals(
      res,
      res.stream()
          .sorted(comparing(PostView::rating, reverseOrder()))
          .collect(Collectors.toList()),
      "Posts should be sorted in by rating in descending order"
  );
  assertTrue(
      res.stream()
          .allMatch(post -> post.comments().size() == 2),
      "Each post should have 2 comments"
  );
}
```

![test results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jditwtifwzjagr0ars38.png)

Indeed, it's easier to understand the flow. And it can work for simple scenarios. But let's assume that there are several cases of creating new posts. That would require adding many overloading methods.

```java
public class PostTestFactory {
  public static Post createPost(User author) {
    ...
  }

  public static Post createPost(double rating, User author) {
    ...
  }

  public static Post createPost(String name, User author) {
    ...
  }

  public static Post createPost(String name, String content, User author) {
    ...
  }

  public static Post createPost(String name, String content, double rating, User author) {
    ...
  }
}
```

What if the `Post` entity had a greater amount of fields? What if some of these attributes were optional? Imagine how many `createPost` declarations would we need to cover all these cases.

The Object Mother Pattern partially solves the problem of arguments' names by reducing its number. Anyway, the solution is far from perfect.

## Test Data Builder

[Test Data Builder](https://www.javacodegeeks.com/2014/06/test-data-builders-and-object-mother-another-look.html) is an alternative for the Object Mother pattern. It's a typical [GoF pattern](https://refactoring.guru/design-patterns/builder). But we're enhancing it a bit for our test cases.

Take a look at `PostTestBuilder`.

```java
public interface Builder<T> {
  T build();
}

public class PostTestBuilder implements Builder<Post> {
  private String name = "post_name";
  private String content = "post_content";
  private double rating = 0;
  private List<Builder<Comment>> comments = new ArrayList<>();
  private Builder<User> author;
  private OffsetDateTime createdAt = OffsetDateTime.now();

  public static PostTestBuilder aPost() {
    return new PostTestBuilder();
  }

  private PostTestBuilder(PostTestBuilder builder) {
    this.name = builder.name;
    this.content = builder.content;
    this.rating = builder.rating;
    this.comments = new ArrayList<>(builder.comments);
    this.author = builder.author;
    this.createdAt = builder.createdAt;
  }

  private PostTestBuilder() {
  }

  public PostTestBuilder withName(String name) {
    final var copy = new PostTestBuilder(this);
    copy.name = name;
    return copy;
  }
  
  // other "with" methods skipped for brevity

  @Override
  public Post build() {
    final var post = new Post();
    post.setName(name);
    post.setContent(content);
    post.setRating(rating);
    post.setComments(
        comments.stream()
            .map(Builder::build)
            .peek(c -> c.setPost(post))
            .collect(toList())
    );
    post.setAuthor(author.build());
    post.setCreatedAt(createdAt);
    return post;
  }
}
```

It looks like a regular builder with a few slight differences. 

Firstly, the class implements the `Builder` interface that provides the single `build` method. You may think that it's an overcomplication but soon you'll realise its benefits.

Secondly, the relational attributes. The builder holds `Builder<User>` instead of `User` itself (and so for other relations). It helps to make the client code less verbose.

And finally, mutators (`withName`, `withContent`, etc.) return a new builder instead of changing the state of the current one. It's a useful feature when we want to create many similar objects that only differ by specific arguments.

```java
var post = aPost().withRating(5).withCreatedAt(date);
var progPost = post.withName("Spring Boot Features").build();
var financePost = post.withName("Stocks for beginners").build();
```

The rewritten test looks like this.

```java
@Test
@DisplayName("Should return top 3 posts")
void shouldReturnTop3Posts() {
  final var user = aUser().withLogin("login");
  final var comment = aComment().withAuthor(user);
  for (int i = 0; i < 10; i++) {
    postRepository.save(
        aPost()
            .withRating(i)
            .withAuthor(user)
            .withComments(List.of(
                comment.withText("comment1"),
                comment.withText("comment2")
            ))
            .build()
    );
  }
  // when, then
}
```

Have you noticed how cleaner the test looks? We're creating 10 posts with a rating of `i`. Each post has 2 comments. Amazing!

But sadly, it does not work.

```
org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation
```

`Post.author` save is not propagated. It can be fixed by `CascadeType.PERSIST` option usage. But perhaps we do not need to cascade the author persistence in the application. So, changing it just to make the test "green" is the wrong path.

## Persisted Wrappers

The `Post` entity and all of its relations (author and comments) are being created on the `build` method invocation. So, we need to persist child objects when the corresponding builder processes the instantiation. Do you remember the additional `Builder` interface? It will help us now.

```java
public class TestDBFacade {
  @Autowired
  private TestEntityManager testEntityManager;
  @Autowired
  private TransactionTemplate transactionTemplate;

  public <T> Builder<T> persisted(Builder<T> builder) {
    return () -> transactionTemplate.execute(status -> {
      final var entity = builder.build();
      testEntityManager.persistAndFlush(entity);
      return entity;
    });
  }
}
```

The `persisted` callback returns the same builder that will persist the resulting entity prematurely.

Now we need to inject `TestDBFacade` into our test suite. This could be done by `@ContextConfiguration` annotation usage.

```java
@SpringBootTest
@AutoConfigureTestEntityManager
@ContextConfiguration(classes = TestDBFacade.Config.class)
class PostRepositoryTestDataBuilder extends AbstractIntegrationTest {
  @Autowired
  private PostRepository postRepository;
  @Autowired
  private TestDBFacade db;
  ...
}
```

We can do this even better by declaring custom annotation.

```java
@Retention(RetentionPolicy.RUNTIME)
@SpringBootTest
@AutoConfigureTestEntityManager
@ContextConfiguration(classes = TestDBFacade.Config.class)
public @interface DBTest {
}
```

So, that's the resulting test.

```java
@Test
@DisplayName("Should return top 3 posts")
void shouldReturnTop3Posts() {
  final var user = db.persisted(aUser().withLogin("login"));
  final var comment = aComment().withAuthor(user);
  for (int i = 0; i < 10; i++) {
    postRepository.save(
        aPost()
            .withRating(i)
            .withAuthor(user)
            .withComments(List.of(
                comment.withText("comment1"),
                comment.withText("comment2")
            ))
            .build()
    );
  // when, then
}
```

> We do not have to wrap comments with `persisted` callback because `Post.comments` relation is already marked with the `CascadeType.PERSIST` attribute.

Now it works.

![test result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0m6flkmho7omlm66p86h.png)

I think you've already guessed that not everything is tuned properly. If you turn on the SQL logging, you can notice it.

```sql
insert into user_table (first_name, last_name, login) values (?, ?, ?)
insert into user_table (first_name, last_name, login) values (?, ?, ?)
insert into user_table (first_name, last_name, login) values (?, ?, ?)
insert into post (author_id, content, created_at, name, rating) values (?, ?, ?, ?, ?)
insert into comment (author_id, created_at, post_id, text) values (?, ?, ?, ?)
insert into comment (author_id, created_at, post_id, text) values (?, ?, ?, ?)
```

We expected to have a single user. But in reality, a new one was created for each post and comment. It means that instead, we have 30 users.

Thankfully, there is a workaround.

```java
public <T> Builder<T> persistedOnce(Builder<T> builder) {
  return new Builder<T>() {
    private T entity;

    @Override
    public T build() {
      if (entity == null) {
        entity = persisted(builder).build();
      }
      return entity;
    }
  };
}
```

The persistence is being processed only on the first call. Latter invocations return the saved entity.

> This method is not thread-safe. Nevertheless, it's acceptable in most cases. Because usually, each test scenario is an independent operation that runs sequentially.

All we have to do is to change `persisted` with `persistedOnce` usage.

```java
@Test
@DisplayName("Should return top 3 posts")
void shouldReturnTop3Posts() {
  final var user = db.persistedOnce(aUser().withLogin("login"));
  final var comment = aComment().withAuthor(user);
  for (int i = 0; i < 10; i++) {
    postRepository.save(
        aPost()
            .withRating(i)
            .withAuthor(user)
            .withComments(List.of(
                comment.withText("comment1"),
                comment.withText("comment2")
            ))
            .build()
    );
  // when, then
}
```

Now there is only one persisted user.

```sql
insert into user_table (first_name, last_name, login) values (?, ?, ?)
insert into post (author_id, content, created_at, name, rating) values (?, ?, ?, ?, ?)
insert into comment (author_id, created_at, post_id, text) values (?, ?, ?, ?)
insert into comment (author_id, created_at, post_id, text) values (?, ?, ?, ?)
```

Another thing to refactor is `PostRepository` usage. We do not have to add additional dependencies on specific Spring Data repositories. We can add the `save` method directly to `TestDBFacade`.

```java
public <T> T save(T entity) {
  return transactionTemplate.execute(status -> {
    testEntityManager.persistAndFlush(entity);
    return entity;
  });
}

@Test
@DisplayName("Should return top 3 posts")
void shouldReturnTop3Posts() {
  final var user = db.persistedOnce(aUser().withLogin("login"));
  final var comment = aComment().withAuthor(user);
  for (int i = 0; i < 10; i++) {
    db.save(
        aPost()
            .withRating(i)
            .withAuthor(user)
            .withComments(List.of(
                comment.withText("comment1"),
                comment.withText("comment2")
            ))
            .build()
    );
  // when, then
}
```

## Clear Assertions

The last thing that I want to discuss is assertions usage. Take a look at the current test again.

```java
assertEquals(3, res.size(), "Unexpected posts count");
assertEquals(
    res,
    res.stream()
       .sorted(comparing(PostView::rating, reverseOrder()))
       .collect(Collectors.toList()),
    "Posts should be sorted in by rating in descending order"
);
assertTrue(
    res.stream()
       .allMatch(post -> post.comments().size() == 2),
    "Each post should have 2 comments"
);
```

The `size` assertion is OK. And what about the sorting and the comments count check? They are not transparent. When you look through the code, you should pay extra attention to figure out the purpose of these assertions.

Anyway, that is not the only problem. What if they failed? We would get something like this in the log.

```
Posts should be sorted in by rating in descending order
Expected : [posts list]
Actual   : [posts list]

Each post should have 2 comments
Expected : true
Actual   : false 
```

Not so descriptive. But there is a better alternative.

### Hamcrest

[Hamcrest](http://hamcrest.org/) is an assertion library that helps to build a declarative pipeline of _matchers_. For example, that's how posts count can be validated.

```java
assertThat(res, hasSize(3));
```

Not so different from the initial attempt. What about two others? Well, these assertions are domain-oriented. Hamcrest does not provide utilities for specific business cases. But what makes Hamrest powerful are custom matchers. Here is one for comments count validation.

```java
private static Matcher<List<PostView>> hasComments(int count) {
    return new TypeSafeMatcher<>() {
      @Override
      protected boolean matchesSafely(List<PostView> item) {
        return item.stream()
            .allMatch(post -> post.comments().size() == count);
      }

      @Override
      public void describeTo(Description description) {
        description.appendText(count + " comments in each post");
      }

      @Override
      protected void describeMismatchSafely(List<PostView> item, Description mismatchDescription) {
        mismatchDescription.appendText(
            item.stream()
                .map(postView -> format(
                    "PostView[%d] with %d comments",
                    postView.id(),
                    postView.comments().size()
                ))
                .collect(joining(" ; "))
        );
      }
    };
  }
```

The `matchesSafely` method executes the assertion itself. In this case, we check that the sorted is equal to the initial one.
The `describeTo` method assigns a label to the assertion.
And the `describeMismatchSafely` logs the output in case of test failure.

The assertion usage is straightforward.

```java
assertThat(res, hasComments(2));
```

Suppose that received posts do not have the expected count of comments. Here is failed test output.

```
Expected: 2 comments in each post
     but: PostView[10] with 3 comments ; PostView[9] with 3 comments ; PostView[8] with 3 comments
```

The intent is obvious now. We expected that every post would have 2 comments but it was 3. Much more expressive than simple `expected true but was false`.

Time to write the matcher for posts sorting order validation.

```java
private static Matcher<List<PostView>> sortedByRatingDesc() {
    return new TypeSafeMatcher<>() {
      @Override
      protected boolean matchesSafely(List<PostView> item) {
        return item.equals(
            item.stream()
                .sorted(comparing(PostView::rating, reverseOrder()))
                .collect(toList())
        );
      }

      @Override
      public void describeTo(Description description) {
        description.appendText("sorted posts by rating in desc order");
      }

      @Override
      protected void describeMismatchSafely(List<PostView> item, Description mismatchDescription) {
        mismatchDescription.appendText(
            item.stream()
                .map(PostView::rating)
                .collect(toList())
                .toString()
        );
      }
    };
  }
```

And here is the assertion.

```java
assertThat(res, is(sortedByRatingDesc()));
```

> `Matchers.is` is a decorator that makes tests look more expressive. Though it can be omitted.

And here is failed test output.

```
Expected: is sorted posts by rating in desc order
     but: [7.0, 8.0, 9.0]
```

Again, the user intent is obvious. We expected to receive `[9, 8, 7]` but got `[7, 8, 9]`.

## Summary

We did some refactoring. Let's compare the initial attempt and the final version.

### The First Attempt

```java
@SpringBootTest
class PostRepositoryTest extends AbstractIntegrationTest {
  @Autowired
  private PostRepository postRepository;
  @Autowired
  private CommentRepository commentRepository;
  @Autowired
  private UserRepository userRepository;

  @Test
  @DisplayName("Should return top 3 posts")
  void shouldReturnTop3Posts() {
    final var user = userRepository.save(new User("login", "", ""));
    for (int i = 0; i < 10; i++) {
      final var post = postRepository.save(
          new Post("name" + i, "content", i, user, OffsetDateTime.now())
      );
      commentRepository.saveAll(
          List.of(
              new Comment(user, "comment1", OffsetDateTime.now(), post),
              new Comment(user, "comment2", OffsetDateTime.now(), post)
          )
      );
    }

    final var res = postRepository.findTopPosts(3);

    assertEquals(3, res.size(), "Unexpected posts count");
    assertEquals(
        res,
        res.stream()
            .sorted(comparing(PostView::rating, reverseOrder()))
            .collect(Collectors.toList()),
        "Posts should be sorted in by rating in descending order"
    );
    assertTrue(
        res.stream()
            .allMatch(post -> post.comments().size() == 2),
        "Each post should have 2 comments"
    );
  }
}
```

### The Final Version

```java
@DBTest
class PostRepositoryTestDataBuilderWithClearAssertions extends AbstractIntegrationTest {
  @Autowired
  private PostRepository postRepository;
  @Autowired
  private TestDBFacade db;

  @Test
  @DisplayName("Should return top 3 posts")
  void shouldReturnTop3Posts() {
    final var user = db.persistedOnce(aUser().withLogin("login"));
    final var comment = aComment().withAuthor(user);
    for (int i = 0; i < 10; i++) {
      db.save(
          aPost()
              .withRating(i)
              .withAuthor(user)
              .withComments(List.of(
                  comment.withText("comment1"),
                  comment.withText("comment2")
              ))
              .build()
      );
    }

    final var res = postRepository.findTopPosts(3);

    assertThat(res, hasSize(3));
    assertThat(res, is(sortedByRatingDesc()));
    assertThat(res, hasComments(2));
  }
}
```

A first option is a group of commands. It's hard to understand what exactly are we testing. Whilst the second approach tells us much more about the behaviour. It's more like documentation that describes the contract of the class.

## Conclusion

Once I heard a wise statement.

> Tests are parts of the code that do not have tests.

It's crucial to test business logic. But it's even more important to make sure that tests won't become a maintainability burden.

That's all I wanted to tell you about writing clear tests. If you have any questions, please, leave your comments down below. Thanks for reading!

## Resources

1. [Spring Boot Testing — Data and Services](https://dev.to/kirekov/spring-boot-testing-data-and-services-288f)
1. [Spring Boot Testing — Testcontainers and Flyway](https://dev.to/kirekov/spring-boot-testing-testcontainers-and-flyway-2jpd)
1. [Repository with code examples](https://github.com/SimonHarmonicMinor/spring-boot-testing-data-patterns)
1. [Object Mother Pattern](https://martinfowler.com/bliki/ObjectMother.html)
1. [Test Data Builder](https://www.javacodegeeks.com/2014/06/test-data-builders-and-object-mother-another-look.html)
1. [Builder Pattern](https://refactoring.guru/design-patterns/builder)
1. [Hamcrest](http://hamcrest.org/)