Sunday, January 31, 2021

testing your backend without getting testy

 Testcontainers is a wonderful library. It allows you to quickly add Docker containers to run services that are needed for your tests. It is the ultimate mock object, since it is nearly identity in function to the service backing your application. In this post, I will use Testcontainers to create a MySQL database to test my Spring Boot application. This uses JUnit 5, Spring Boot 2.4.2, and the current release of MySQL. Full source is here.

Saturday, January 23, 2021

a beautiful option

Recently, I have seen examples of the use and misuse of Java's humble Optional that reduced code readability. One just example was an article on Medium, and showed a few examples in which the code was redundant and verbose, and generally difficult to follow. And while you can misuse Optional in this way, it does not have to result in convoluted code. Instead we can apply the same functional principles that gave us the Optional in the first place and create beautiful code Code that is clean and concise, uncluttered by null checks and if statements.

In this article, I will borrow the same scenario - building a shelf of book's authors - to show that readability can be increased, insomuch as something as subjective as code readability can be.
You have a Book object which you pass to a buildBookshelf method, which returns an enriched Shelf object. 

The Book class, might be defined like this:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {
private Long id;
private String name;
private Optional<Long> authorId;
}

While Author, I presume, was defined something like this:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Author {
private Long id;
private Optional<String> name;
}

If you wanted to get the Author object to add to the Shelf, and were not using JPA to find the Author, you might do the following:

var authorOptional = repository.findById(authorId);

The example next made a check to see that the Optional was not empty, then used the get() method to return the Author, and again for the Author's name.

However, Optional has a method tailor made to make this code more concise.

public Shelf buildShelf(final Book book) {
var shelf = new Shelf();
book.getAuthorId().ifPresent(authorId -> {
var authorOptional = repository.findById(authorId);
authorOptional.ifPresent(author -> {
author.getName().ifPresent(authorName -> {
shelf.addAuthorName(authorName);
});
});
});

return shelf;
}

That is not terrible. It is null safe, and fairly readable. However, it does have a faint, inoffensive odor, reminiscent of the one that waifs off of a callback hell code block. 

I think that we can do better. 

And we can, however, we should not merely think of Optional as a stand-in for a class that can be nullable. It is far more versatile than just a wrapper for null. It is a functional construct, and we should use it as such. We should afford it the same respect as any Collection. In fact, it helps to think of it more as a Collection of zero or one elements. Then we can unlock its true potential. 

Like a stream of a Collection, Optional has the method map that will allow you to apply a Function to each element to transform it to some other output. If we took our nested Optionals and construct a chain of maps instead, then we could use this to “lift” the Optionals to continue the computation, if there are values, or short circuit the computation if any one is empty.

public Shelf buildShelf2(final Book book) {
var shelf = new Shelf();
book
.getAuthorId()
.map(repository::findById)
.orElse(Optional.empty())
.map(Author::getName)
.orElse(Optional.empty())
.ifPresent(shelf::addAuthorName);
return shelf;
}

This is better. The whiff of callback hell is diluted, while preserving the null-safety of the previous attempt. However, the interlaced orElses obfuscates the intent of the code. You could add a comment, but that would be tantamount to an admission of failure. 

But can we do better still?

We can, but first, a slight detour.

Scala has this wonderful language construct called the for comprehension. It is very different than a for loop in Java. It can be used to do exactly what we are looking to do: chain together computation or halt computation if it encounters an empty Option -  Scala’s equivalent to the Optional – which is called None. If we were defining this method in Scala, we might do it like this. 

def bookShelf(book: ScalaBook) : ScalaShelf = {
val name = for {
authorId <- book.authorId
author <- repository.findById(authorId)
name <- author.name
} yield name

val shelf = new ScalaShelf()
shelf addAuthorName name
shelf
}

Sheer elegance. Clear intentions. 

Now there is a little slight of hand embedded in there. I changed the definition of Shelf::addAuthorName to accept an Option[String], rather than just a String. 

But why shouldn’t we do that? 

def addAuthorName(name : Option[String]) : Unit = {
name match {
case Some(authorName) => addAuthorName(authorName)
case None =>
}
}

Here, I match the types of Option that it might receive, either Some, which has a value, or None, which does not. If it receives a Some then it adds the author’s name that the Some wraps to its Set of names. 

By now, hopefully, you are wondering why Java withholds this beautiful functionality from us. If only we need not care if our computation encounters an empty Optional. It would just handle it gracefully, and not throw an NPE. 

Well, we can get the same effect in Java, using Optional’s flatMap method. In fact, Scala’s for comprehension is more or less just doing that. 

We can try to construct this method using flapMap, it might look something like this:

public Shelf buildShelf3(final Book book) {
var shelf = new Shelf();
var authorName = book
.getAuthorId()
.flatMap(repository::findById)
.flatMap(Author::getName)
;

shelf.addAuthorName(authorName);
return shelf;
}

We will have to go back and update our Shelf class to have its method addAuthorName accept an Optional<String>, but I ask you again, why would we not do this?

public void addAuthorName(final Optional<String> optionalAuthorName) {
    optionalAuthorName.ifPresent(authorNames::add);
}

Conversely, we might accept nulls and test for them prior to adding the author's name. Or we might add the ifPresent to the end of our computation.

public Shelf buildShelf4(final Book book) {
var shelf = new Shelf();
book
.getAuthorId()
.flatMap(repository::findById)
.flatMap(Author::getName)
.ifPresent(shelf::addAuthorName);

return shelf;
}

The choice is yours. Happy hacking.