Heart containing Coding Chica Java 101

Lombok NonNull

TIP: References Quick List

Introduction

Similar to the getters and setters, Lombok allows us to hide some of the boilerplate for null checking in our source code, too. In this post, we’ll setup unit tests and our own null checks. Then, we will replace that with a Lombok annotation.

Table of Contents

  1. Introduction
  2. Table of Contents
  3. TDD Cycle 1 – Adding Vanilla Java Logic in Constructor
    1. Unit Test Updates
    2. Maven Build Failure
    3. Runtime Updates
    4. Maven Build Success
  4. TDD Cycle 2 – Changing Constructor to Use Lombok NonNull
    1. Unit Tests
    2. Runtime Updates
    3. Maven Build Failure
    4. Runtime Updates
    5. The Need For Unit Tests for Lombok Generated Code
    6. Maven Build Success
  5. TDD Cycle 3 – The Setter
    1. Unit Test Updates
    2. Runtime Changes
    3. Maven Build Success
  6. Commit

TDD Cycle 1 – Adding Vanilla Java Logic in Constructor


Unit Test Updates

In our src/test/java/codingchica.java101.AppTest, let’s add a unit test for the constructor to enforce that the name provided is not null. There are a few conventions for this, we could either throw a NullPointerException or another exception that is often used in this situation is an IllegalArgumentException. Either way, we should include a message indicating which parameter is null. Right now, we only have one parameter, but that may change in the future. Let’s use an IllegalArgumentException, so we can see how that would later be configured with Lombok.

@ParameterizedTest
@NullSource
void name_whenNullInConstructor_thenExceptionThrown(String name) {
  // Setup

  // Execution
  Executable executable = () -> new App(name);

  // Validation
  Exception exception = assertThrows(IllegalArgumentException.class, executable);
  assertEquals("newName is marked non-null but is null", exception.getMessage());
}

We only have one scenario here – the name being null – but I wanted to show you another JUnit 5 enum for use with parameterized tests – @NullSource. In this case, name will be populated with null when this test is run. In our validation steps, we confirm that the desired exception type is thrown and that the message is the one we expect.

Maven Build Failure

At this point, if we were to run the AppTest in either the IDE or as part of the Maven build, we would see an error like the following:

[ERROR] Tests run: 7, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.019 s <<< FAILURE! -- in codingchica.java101.AppTest$NameTest
[ERROR] codingchica.java101.AppTest$NameTest.getName_whenNullInConstructor_thenExceptionThrown(String)[1] -- Time elapsed: 0.005 s <<< FAILURE!
org.opentest4j.AssertionFailedError: Expected java.lang.IllegalArgumentException to be thrown, but nothing was thrown.

Runtime Updates

Now, let’s update the constructor in src/main/java/codingchica.java101.App to throw the desired exception:

public App(final String newName) {
  if (newName == null) {
    throw new IllegalArgumentException("newName is marked non-null but is null");
  }
  this.name = newName;
}

As you can see, this added significant bloat to our single-line constructor. It quadrupled in length.

Maven Build Success

After the runtime updates, though, our Maven build should show success.

TDD Cycle 2 – Changing Constructor to Use Lombok NonNull


Unit Tests

We have no updates to our unit tests this cycle, as the constructor’s unit test for the name being null was already created.

Runtime Updates

Let’s start by updating the constructor in src/main/java/codingchica.java101.App to instead use the @NonNull Lombok annotation:

import lombok.NonNull;

...

public App(final @NonNull String newName) {
  this.name = newName;
}

This time, we have reduced our constructor’s body back down to a single line. The new @NonNull annotation is in-line with the newName parameter declaration as part of the constructor’s signature, so when looking at the code, I can quickly see that the null check is already taken care of by Lombok. All of these changes make the code easier to read.

Maven Build Failure

However, if we run the unit test or the build at this point, we see a failure:

[ERROR] Tests run: 7, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.017 s <<< FAILURE! -- in codingchica.java101.AppTest$NameTest
[ERROR] codingchica.java101.AppTest$NameTest.getName_whenNullInConstructor_thenExceptionThrown(String)[1] -- Time elapsed: 0.006 s <<< FAILURE!
org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: <java.lang.IllegalArgumentException> but was: <java.lang.NullPointerException>

Lombok isn’t throwing the IllegalArgumentException that we want. Instead, it is throwing a NullPointerException. Lombok does support configuration to change this behavior, but it doesn’t go in the Java file we’re modifying.

Runtime Updates

Let’s add a new file in the java101 folder, called lombok.config. Then, let’s add some contents to that file to change Lombok’s behavior:

# See https://projectlombok.org/features/configuration
lombok.nonNull.exceptionType = IllegalArgumentException

The top line is a comment, just in case we need to make additional modifications to this configuration file in the future. The second line changes the behavior of all @NonNull annotations within the entire project.

The Need For Unit Tests for Lombok Generated Code

This is another reason I still like having unit tests for Lombok generated code. By changing one line in a configuration file, I can change the behavior of these annotations throughout my entire project. Does that new behavior still match my expectations? Am I potentially breaking public API documentation for classes off in remote folders somewhere? Without unit tests to alert me to the per-method behavior changes, I wouldn’t necessarily know.

Granted, if making a change to a configuration file in one project, I could search that project’s folders and files for any mention on @NonNull and go read through the Javadoc for each impacted file. However, what if:

  • Someone has setup a symbolic link to this lombok.config file (meaning that the config file looks like it is in another directory, but it is really just a synced-copy of this file) and I’m also, unknowingly, changing the behavior for another sub-module in my Maven build?
  • What if you are working in a sub-module and someone adds a lombok.config file to the parent folder, not realizing it will bubble down to all Maven sub-modules / child projects?
  • What if a future Lombok version also supports reading this configuration from the classpath and a dependency has their own configuration file erroneously bundled into their jar?

Then, there are all of the reasons we discussed in a prior post. My recommendation continues to be to include unit tests for generated code. Unit tests help protect us from situation when what we are trying to tell the computer to do differs from what we have actually told it to do. If I am responsible for the behavior of that code, it needs a unit test.

Maven Build Success

Now, if we run the unit test or the Maven build, we should see success.

TDD Cycle 3 – The Setter


Lombok is already being used to generate a setter method, so there is nothing for us to update with the vanilla Java approach. Therefore, let’s dive into using Lombok directly for the setter’s behavior.

Unit Test Updates

This time, let’s use the regular @Test approach with no parameters, and pass the null value directly into the setName method. We have already demonstrated the @NullSource annotation in the constructor’s unit test, and parameterized tests have additional overhead compared to regular unit tests.

@Test
void name_whenNullInSetter_thenExceptionThrown() {
  // Setup
  App app = new App("Junie");

  // Execution
  Executable executable = () -> app.setName(null);

  // Validation
  Exception exception = assertThrows(IllegalArgumentException.class, executable);
  assertEquals("name is marked non-null but is null", exception.getMessage());
}

We are using the same general validation details, except the parameter name has changed to remove the ‘new’ prefix.

Runtime Changes

For the runtime changes, we only need to add the new @NonNull annotation to the name field.

/** The name of the user of the application. */
@Getter @Setter @NonNull private String name;

Maven Build Success

Now, if we run the unit test and Maven build again, we should see success.

Commit

Remember to commit in small, incremental steps. Now is a good time.

Lombok NonNull

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.