Lesson 2 - Java Testing - The first unit test in JUnit
In the previous lesson, Introduction to software testing in Java, we made a relatively solid introduction to the issue. We also introduced the v-model that illustrates the relationship between the individual design phase outputs and the relevant tests.
We always write tests based on design, not the implementation. In other words, we create them based on the expected functionality. It can be obtained either directly from the customer (and that's in the case of acceptance tests) or from the programmer (architect), where he specifies how different methods should behave. Today, we're going to focus on these tests, that are called unit tests, and which test the detailed specification of the application, thus its classes.
Remember that we never write tests according to how something is programmed inside! It could very easily seduce our thinking in just one way, and we could forget that the method might get some other inputs which it isn't expecting. In fact, testing isn't related with implementation at all, we always test whether the requirements are met.
Which classes to test
Unit tests test individual methods in classes. For sure, I repeat that it doesn't make much sense to test single-purpose methods, for example, in beans or JavaFX applications that just pick something from the database. To be more specific, there is no point in testing a method like this:
public void insertItem(string name, double price) { try (Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/app_db?user=root&password="); PreparedStatement statement = connection.prepareStatement("INSERT INTO item (name, price) VALUES (?, ?)");) { statement.setString(1, name); statement.setDouble(2, price); } catch (SQLException ex) { System.err.println("Error while communicating with the database"); } }
The method inserts an item to the database. Typically, it is used only in a form, and if it does not work, acceptance tests will find it out as the new item would not appear on the list. There are lots of similar methods in the application and we would unnecessarily waste time by covering something that we can easily cover in other tests.
Unit tests are most often found with libraries, that is, the
tools that the programmer uses in more places or even in
multiple projects and should be 100% functional. You may remember when you used
a library downloaded, for example, from GitHub. Most likely, there were also
tests included which are most often in the test/
folder, next to
the src/
folder in the project's directory structure.
For example, if we write an application in which we often need some mathematical calculations, such as factorials and other probability functions, we create a library, for sure, for these calculations, and it's a good idea to cover such library with tests.
Example
As you probably expect, we'll create such a class and test it. In order to save time, let's just create a simple calculator which will be able to:
- add
- subtract
- multiply
- divide
Creating a project
In practice, there would be more complicated calculations in the class, but
we won't deal with it here. Create a new project named UnitTests
.
Add a class named Calculator
with the following implementation:
package unittests; /** * Represents a simple calculator */ public class Calculator { /** * Sums 2 numbers * @param a The first number * @param b The second number * @return The sum of 2 numbers */ public double add(double a, double b) { return a + b; } /** * Subtracts 2 numbers * @param a The first number * @param b The second number * @return The difference of 2 numbers */ public double subtract(double a, double b) { return a - b; } /** * Multiplies 2 numbers * @param a The first number * @param b The second number * @return The product of 2 numbers */ public double multiply(double a, double b) { return a * b; } /** * Divides 2 numbers * @param a The first number * @param b The second number * @return The quotient of 2 numbers */ public double divide(double a, double b) { if (b == 0) throw new IllegalArgumentException("Cannot divide by zero!"); return a / b; } }
The only interesting thing about the code is the divide()
method, which throws an exception if we divide by zero. The default Java
behavior for decimal numbers would be to return the "Infinity" value, which
always isn't what the application user expects.
Generating the tests
In Java, we use the JUnit framework for tests. This should be a part of NetBeans, if you don't have it installed, you can simply add it in the Tools -> Plugins menu. In the left Projects panel, right-click on the project and choose New -> Other.
In the next dialog box we'll select the Unit Tests category and the JUnit Test file type. By this, we're saying we're adding a new unit test for a class.
In the last window, we'll enter the name of the test, which is usually the
name of the class we're testing + the word Test
, so in our case
it's CalculatorTest
. We usually add tests to the "Test Packages"
package.
We'll confirm and a new file with the following code will be generated for us:
import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; import static org.junit.Assert.*; public class CalculatorTest { public CalculatorTest() { } @BeforeClass public static void setUpClass() { } @AfterClass public static void tearDownClass() { } @Before public void setUp() { } @After public void tearDown() { } // TODO add test methods here. // The methods must be annotated with annotation @Test. For example: // // @Test // public void hello() {} }
Perhaps you aren't surprised that, in object-oriented Java, class tests
(scenarios) are also represented by a class and the individual tests by methods
What is more interesting is
that we can already find several pre-generated methods in the class, which are
marked with annotations. setUpClass()
is called once at the start,
before all the tests. tearDownClass()
works similarly, it's called
once at the end when all the tests are done.
Class coverage with tests
The setUp()
and tearDown()
methods, more precisely
the methods with the @Before
and @After
annotations
will be called before, respectively, after each test in this
class. This is very important to us because, according to best practices, we
want our tests to be independent. Usually, before each test, we
prepare the same environment again to avoid any interference of the tests with
each other. We'll discuss good practices in detail in the next lesson. Let's add
a calculator
field to the class and, in the setUp()
method, we'll always create a new calculator instance for each test. If there
were any additional settings or dependencies needed, they would be in this
method as well.
public class CalculatorTest { private Calculator calculator; public CalculatorTest() { } @BeforeClass public static void setUpClass() { } @AfterClass public static void tearDownClass() { } @Before public void setUp() { // A new calculator is always created before each test to guarantee their independence calculator = new Calculator(); } ...
Since the tests are in a different package than the application
classes, import the Calculator
class by clicking the icon left to
the given line.
We have everything ready to add tests. Each method will always be marked with
the @Test
annotation and will test one particular method from the
Calculator
class, typically for several different inputs. If you're
wondering why we mark the methods with annotations, it allows us to create some
auxiliary methods that we can use in the test and which will not be considered
as tests. This is because NetBeans runs the tests (the methods with the
@Test
annotation) automatically and prints their results. In older
JUnit versions, the methods had to start with "test"
instead of
have an annotation, and the class inherited from the scenario (the
TestCase
class).
Let's add the following 5 methods:
@Test public void add() { assertEquals(2, calculator.add(1, 1), 0); assertEquals(1.42, calculator.add(3.14, -1.72), 0.001); assertEquals(2.0/3, calculator.add(1.0/3, 1.0/3), 0.001); } @Test public void subtract() { assertEquals(0, calculator.subtract(1, 1), 0); assertEquals(4.86, calculator.subtract(3.14, -1.72), 0.001); assertEquals(2.0/3, calculator.subtract(1.0/3, -1.0/3), 0.001); } @Test public void multiply() { assertEquals(2, calculator.multiply(1, 2), 0); assertEquals(-5.4008, calculator.multiply(3.14, -1.72), 0.001); assertEquals(0.111, calculator.multiply(1.0/3, 1.0/3), 0.001); } @Test public void divide() { assertEquals(2, calculator.divide(4, 2), 0); assertEquals(-1.826, calculator.divide(3.14, -1.72), 0.001); assertEquals(1, calculator.divide(1.0/3, 1.0/3), 0); } @Test(expected=IllegalArgumentException.class) public void divideException() { calculator.divide(2, 0); }
We use assert*()
methods imported statically from the
org.junit.Assert
package to compare the output of the method with
the expected value. You will most likely use the assertEquals()
method, which accepts the expected value as the first parameter and the actual
value as the second parameter. It's a good idea to maintain this order,
otherwise, you'll have the values swapped in the test results. As you probably
know, decimal numbers are stored in binary in computer memory (obviously ) and this causes some loss of
their accuracy and also difficulties when comparing them. Therefore, we have to
provide the third parameter in this case, which is delta, a
positive tolerance, how much the expected and actual value may vary for the test
to be successful.
Note that we try various inputs. We do not only test the addition as 1 + 1 = 2, but we test the integer, decimal, and negative inputs separately and verify the results. In some cases, we might also be interested in the maximum value of the data types and similar borderline values.
The last test verifies whether the divide()
method really throws
an exception when dividing by zero. As you can see, we don't have to bother with
the try
-catch
blocks, we just need to add the
expected
parameter to the annotation and specify which exception
class is expected there. If the exception doesn't occur, the test fails. We'd
have to add more methods to test multiple situations throwing expections. We'll
get back to this in the next lesson.
Available assert methods
Besides the assertEquals()
method, we can use a few more. Always
try to use the most suitable method, it makes the error messages clear when the
test fails and, of course, it's easier to fix it.
assertArrayEquals()
- Checks whether 2 arrays contain the same elements.assertEquals()
- Checks whether 2 values are the same (compares withequals()
).assertFalse()
- Checks whether the value isfalse
.assertNotEquals()
- Checks whether 2 values are not the same.assertNotNull()
- Checks whether the value is notnull
.assertNotSame()
- Checks whether 2 references point to the same object.assertSame()
- Checks whether 2 references point to the same object (compares with==
).assertTrue()
- Checks whether the value istrue
.
The assertThat()
method supports a newer approach to writing
assertions, but we'll explain that next time.
Running the tests
We'll run the tests by right-clicking on the project and choosing "Test" from the context menu.
NetBeans will give us a nice visual overview of the test progress (ours will be done instantly) and the results. Use the green check mark to display the passed tests as well.
Let's now make a mistake in the calculator, for example, comment out throwing an exception when dividing by zero:
public double divide(double a, double b) { //if (b == 0) // throw new IllegalArgumentException("Cannot divide by zero!"); return a / b; }
And let's run our tests again:
We can see that the error is detected and we are notified about that. We may revert the code to its original state. Next time, in the leson Java Testing - Hamcrest, JUnit TestRule and Best Practices, we'll look at the changes in JUnit, introduce the Hamcrest library, learn how to apply rules, and mention the most important best practices for writing tests.
Did you have a problem with anything? Download the sample application below and compare it with your project, you will find the error easily.
Download
By downloading the following file, you agree to the license terms
Downloaded 33x (32.43 kB)
Application includes source codes in language Java