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:
Annotation | Description |
@Test | This annotation tells JUnit which methods should be run as tests. |
@Before | This annotation tells JUnit to run the marked methods before each test method, in order to create objects needed for them to work. |
@After | This 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. |
@BeforeClass | This annotation makes the marked method execute only once, before all test methods. |
@AfterClass | This annotation makes the marked method execute only once, after all test methods. |
@Ignore | This 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:
assertEquals | Checks if two primitive types or objects are equal |
assertTrue | Checks if input condition is true |
assertFalse | Checks if input condition is false |
assertNotNull | Checks if an object isn't null |
assertNull | Checks if an object is null |
assertSame | Checks if two object references point to the same object |
assertNotSame | Checks if two object references do not point to the same object |
assertArrayEquals | Checks 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.