How to write tests in Java - JUnit

Writing tests and test cases is important in software engineering, however, it seems that a lot of people, especially new-baked developers are afraid of tests or simply evade to put them into practice. 

 Need for unit testing

Testing allows you to see if each small piece of code produces the desired outcome and if it works as intended. These small pieces of code are called units, and they can't get any simpler than that. It's because of their small and isolated nature that makes it so easy to fix a problem if it arises.

Most bugs, holes, and oversights in programs are noticed during runtime. Unit testing allows for the automation of the testing process and helps you pinpoint the offending code which would usually be hidden behind a complex architecture, posing a seemingly much greater problem. 

JUnit and TestNG are the most widespread unit testing frameworks these days, and we will be committing our time to the former in this article.

 Standard unit testing practices

There are some standards to follow while writing unit tests.

Unit test location - Typically, we put Java classes into src/main/java while we put test classes in src/test/java

Naming Conventions - It's standard practice to name test classes the same as the classes being tested with the addition of "Test" at the end. Ex: "MainController" and "MainControllerTest". Maven took advantage of this convention and includes all classes with this suffix in its test scope.

Unit test information - Providing meaningful and useful messages during tests is crucial. If you're working in a team, allowing others to understand your tests is highly commendable.

Method Naming Convention - When writing test methods, there are multiple approaches:

  • should[action]
    Example: mailShouldBeSent or cartShouldGetCleared
  • should[consequence]when[action]
    Example: shouldBanWhenEULAIsBroken
  • Given[input]When[action]Then[consequence] 
    Given_UserIsLoggedIn_When_SessionIsExpired_Then_LogoutUser


Annotations

JUnit has introduced us to some new annotations as well:

AnnotationDescription
@TestThis annotation tells JUnit which methods should be run as tests.
@BeforeThis annotation tells JUnit to run the marked methods before each test method, in order to create objects needed for them to work.
@AfterThis annotation is used to clean up and release any resources that you might have used in the test. These methods will be run after each test method.
@BeforeClassThis annotation makes the marked method execute only once, before all test methods.
@AfterClassThis annotation makes the marked method execute only once, after all test methods.
@IgnoreThis annotation is used to ignore test methods and will tell JUnit not to run them.


Note that JUnit 5 introduces @BeforeEach and @BeforeAll instead of @Before and @BeforeClass respectively, as well as @AfterEach and @AfterAll. These annotation names are more indicative and cause less confusion.

 Using JUnit

JUnit tests are simply segregated methods in a test class. To define a method to be a test method, we annotate it with the @Test annotation.

By using the assert method, you can check a result and compare it to an expected result. Generally, these methods are referred to as asserts or assert statements, and you will find these terms used interchangeably in literature.

Examples:

assertEqualsChecks if two primitive types or objects are equal
assertTrueChecks if input condition is true
assertFalseChecks if input condition is false
assertNotNullChecks if an object isn't null
assertNullChecks if an object is null
assertSameChecks if two object references point to the same object
assertNotSameChecks if two object references do not point to the same object
assertArrayEqualsChecks whether two arrays are equal to each other


Let's create a class with a simple method that adds two numbers:

public class Main {

public static int addNumbers(int x, int y) {

int result = x + y;

return result;

}

}

To test if this code runs successfully and as expected, we make a new test class, paying attention to conventions above:

public class MainTest {

@Test

public void shouldReturnTwenty() {

Main testMain = new Main();

assertEquals("15 + 5 must return 20", 20, testMain.addNumbers(5, 15));

}

}

Our assertEquals method accepts a message if the test fails, an expected int, and the actual result. In our case, we are expecting the method to return 20, so this check runs successfully.

Process finished with exit code 0

On the other hand, if we modify the test like so:

public class MainTest {

@Test

public void shouldReturnTwenty() {

Main testMain = new Main();

assertEquals("15 + 5 must return 20", 25, testMain.addNumbers(5, 15));

}

}

Our test fails and we are greeted with our message:

java.lang.AssertionError: 15 + 5 must return 20

Expected :25

Actual :20

<Click to see difference>

at org.junit.Assert.fail(Assert.java:88)

at org.junit.Assert.failNotEquals(Assert.java:834)

at org.junit.Assert.assertEquals(Assert.java:645)

at test.MainTest.shouldReturnTwenty(MainTest.java:15)

...

Process finished with exit code -1

 JUnit Test Fixture

Let's simply lay out a standard test fixture, and how a unit looks like:

public class MainTest {

private static Main testMain;

@BeforeAll

public static void setupClass() {

testMain = new Main();

}

@BeforeEach

public void setupForEachMethod() {

...code

}

@Test

public void shouldReturnTwenty() {

assertEquals("15+5 must return 20", 20, testMain.addNumbers(5, 15));

}

@Test

public void shouldReturnThirty() {

assertEquals("25+5 must return 30", 30, testMain.addNumbers(25, 5));

}

@Test

public void shouldReturnSomething() {

...code

}

@AfterEach

public void afterEachTest() {

...code

}

@AfterAll

public static void afterAllTests() {

...code

}

}

 It's arguable which methods should be tested. Some argue that all code should be tested in such classes, while some argue that's it's unnecessary for some methods, but everybody agrees that you should write tests for critical parts of code, especially if it's a newly developed feature.
 In our example, we tested our method with two tests - shouldReturnTwenty and shouldReturnThirty.

 Conclusion

In this article, we went over the need for testing, standard practices, naming conventions, and a test fixture, got familiar with annotations and methods from the JUnit framework and wrote our own test.