Additional Info:
Table of Content
- Table of Content
- Introduction
- Add Star Imports
- Approach
- New Nested Test Class
- New Test Method
- Application Changes – New Method
- Run Unit Test – Failure
- Update Greeting Value
- Update Main Method
- Clean Up Test Class Imports
- Test Via Application Run
- Commit
Introduction
Let’s start by writing a test for the change we want to make.
Wait, we haven’t written any code yet, why are we writing our first test? Test-Driven Development is where you write the test(s) first, then make just enough changes to the runtime code to make those test succeed, repeat as needed. This is a good practice for all developers, but especially important if you are unsure of the changes you are making.
Automated tests generate a contract with either yourself or the next developer who works on that code. As long as the method behaves this way, then it isn’t broken and your changes to existing behavior are likely OK. This allows us to use a successful Maven build as confirmation that we’re ready to commit a change.
However, this approach is based upon everyone adding unit tests as they go. If we don’t have good code coverage, then we can’t simply rely on the build’s unit test for basic validation.
Let’s update the existing import lines to make them a little more flexible for the time being:
Add Star Imports
Let’s change our existing imports section at the top of the codingchica.java101.AppTest class. You can make this change with or without the comments. Add them if you find them a helpful reminder.
// The * imports all classes in the org.junit.jupiter.api package.
// See also: https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/package-summary.html
import org.junit.jupiter.api.*;
/*
* The * imports all static methods within the
* org.junit.jupiter.api.Assertions class.
* See also: Static Methods section within https://junit.org/junit5/docs/current/api/org.junit.jupiter.api/org/junit/jupiter/api/Assertions.html
*/
import static org.junit.jupiter.api.Assertions.*;
Note: We will want to clean this up before we commit, otherwise the compiler may bring in a lot of dependencies that we don’t actually use in the code, but while we are drafting the change, this makes things a bit easier.
Approach
Testing command line output is difficult. Instead, let’s create a new method that returns the expected greeting. Then, we can just update the main method to output that value, rather than the currently hard-coded one. While we aren’t actually testing the output generated, we’re testing all but that last step.
New Nested Test Class
Everyone has their own style. What I like to do is create a nested class (a JUnit 5 feature) inside of the outer class for each method being tested. That way, they stay grouped together no matter what.
Let’s remove the old test that doesn’t actually test the our code…and replace it with our own nested class. We also need to add the @Nested tag to the new, nested class, so that JUnit will look for tests within it.
public class AppTest {
/**
* Unit tests for the getGreeting method.
* @see App#getGreeting()
*/
@Nested
class GetGreetingTest {
}
}
New Test Method
Inside of the GetGreetingTest class, let’s create a new test method. Unlike JUnit 4, we don’t have to name the method in any particular way, but we do have to add a @Test annotation to it.
@Nested
class GetGreetingTest {
@Test
void getGreeting_whenInvoked_thenExpectedResultReturned() {
// Setup
String expectedGreeting = "Hello, gorgeous!";
App app = new App();
// Execution
String actualGreeting = app.getGreeting();
// Validation
assertEquals(expectedGreeting, actualGreeting, "greeting");
}
}
Return Type – Void
By starting the new method with void, we’re saying that nothing is returned from it. With JUnit, just completing the method call successfully means that the test was successful. The logic will be halted by the assertEquals if the test fails during the validation step.
Method Name
Comments in tests often get overlooked during updates and inaccurate documentation can be just as bad as no documentation. Instead, I like to use a test method naming format like:
- <methodName>_when<Condition>_then<Result>
This has an added benefit of throwing a compiler error if I accidentally have 2 tests for the same scenario (such as a copy/paste error), rather than slowing down the build with no benefit. Even if the test runs very quickly, it otherwise is still an extra time taken each and every build.
No Method Parameters
For simple tests like this one, JUnit will not provide any input parameters when the test is invoked. We use the () after the method name to specify that they are not needed.
Method Comments
Both as sign-posts for the reader, as well as forcing myself to only have a single test per method, I like to add comments in my test methods like:
// Setup
// Execution
// Validation
Those familiar with Behavior Driven Development’s (BDD) acceptance criteria keywords might instead choose names, like:
// Given
// When
// Then
Both are equivalent in my opinion. As long as the test is easy to read and only tests one scenario, then it is likely a win.
Test Setup
The setup is what needs to happen before we are ready to test. This should be independent of all other tests. In the future, these unit tests may be run in parallel, so we want them isolated.
Also, there is only one test at present, so we aren’t worried about code reuse. That will come later.
/*
* This line defines a local variable within the test that specifies
* what we expect the output of the method call to look like. It is
* a String with the value Hello, gorgeous!
*
* Notice that each command ends with a semi-colon (;).
* This is required in Java.
*/
String expectedGreeting = "Hello, gorgeous!";
/*
* This is a call to the no-arg (AKA no-argument/parameter) version
* of the constructor for the App class. There isn't one defined in
* the App class, but Java gives you one by default.
*
* This command then stores that instance into a local variable
* called app.
*/
App app = new App();
An instance of a class is what it sounds like. If the class were for a Cat, then an instance of that class might be a particular cat named Garfield.
Test Execution
Since we are writing the test before the actual program’s changes, if we tried to run the test right now, the code wouldn’t compile – because there is no getGreeting method inside of the App class.
/*
* Invoke the getGreeting method within the app variable.
* Then, store the output of that method call to a String variable
* called actualGreeting.
*/
String actualGreeting = app.getGreeting();
Once we make the changes in the codingchica.java101.App class, then we will be able to run this test.
Test Validation
The validation section is small, but powerful. When invoked, the assertEquals(expected, actual, message) method will either:
- Confirm that the expected and actual values are equivalent, or
- Stop the test and mark it as a failure
assertEquals(expectedGreeting, actualGreeting, "greeting");
Application Changes – New Method
Now that we have a test setup, let’s update the application enough that we can run the test and get a failure out of it.
Let’s add the new method to codingchica.java101.App, but not return the expected value, yet:
public String getGreeting() {
return "";
}
In the code above:
- public means it can be invoked from anywhere (more on that later)
- String is what type of output to expect when we invoke the method
- getGreeting is the name of the method
- () means that we consume no input parameters
- return “”; means that when the method runs, we will return an empty string (one with no characters).
- { and } show the start and end of the method’s body
Run Unit Test – Failure
Now we should be able to run the new unit test, either in the IDE or via the Maven build. By running it in the Maven build, we also confirm that if this test fails, the Maven build will also be marked as a failure.
Example Maven build output:
[ERROR] Tests run: 1, Failures: 1, Errors: 0, Skipped: 0, Time elapsed: 0.027 s <<< FAILURE! -- in codingchica.java101.AppTest$GetGreetingTest
[ERROR] codingchica.java101.AppTest$GetGreetingTest.getGreeting_whenInvoked_thenExpectedResultReturned -- Time elapsed: 0.019 s <<< FAILURE!
org.opentest4j.AssertionFailedError: greeting ==> expected: <Hello, gorgeous!> but was: <>
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
...
Update Greeting Value
Now, if we change the “” to instead be “Hello, gorgeous!” we will have completed the expectations for the new test to succeed.
public String getGreeting() {
return "Hello, gorgeous!";
}
If we now rerun the Maven build, we should see success.
Update Main Method
If we were to run the application right now, the behavior wouldn’t have changed, even though our unit tests pass. Although we generally want to do test driven development, let’s go ahead and change the main method to call the new getGreeting method for now…and add the new test later. It will take some additional changes, which also need to be explained.
public static void main(String[] args) {
// Create a new App instance and
// store it in a variable named app
App app = new App();
// Invoke the new getGreeting() method within the app
// instance and use it for the output, rather than the
// previously hard-coded value.
System.out.println(app.getGreeting());
}
You can include the comments, if desired. However, I just included them so you can understand what is being done.
Clean Up Test Class Imports
Now that we’re about ready to commit, let’s clean up those star (wild-card) imports from earlier.
IntelliJ: In the Project view -> Right click on AppTest class name -> Optimize Imports.
The imports section of the AppTest class should be changed to only include what is currently being used:
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
Test Via Application Run
Since there is no JUnit test currently checking the updated main method’s behavior, let’s manually validate that change by:
- Performing a Maven build to create a jar (mvn clean install)
- Running the jar’s App class (For example: java -cp target/java101-0.1-SNAPSHOT.jar codingchica.java101.App)
The output generated should now be updated to Hello, gorgeous!.
Commit
Remember, commit after small, incremental changes are complete.

Leave a comment