Pre-Christmas sale Front-end week
Christmas are almost here! Get up to 20 % extra points for free! More info
Save up to 80 % on HTML & CSS + JavaScript e-learning! Only this week!

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 with equals()).
  • assertFalse() - Checks whether the value is false.
  • assertNotEquals() - Checks whether 2 values are not the same.
  • assertNotNull() - Checks whether the value is not null.
  • 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 is true.

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.


 

Download

Downloaded 0x (32.43 kB)
Application includes source codes in language Java

 

 

Article has been written for you by David Capka
Avatar
Do you like this article?
No one has rated this quite yet, be the first one!
The author is a programmer, who likes web technologies and being the lead/chief article writer at ICT.social. He shares his knowledge with the community and is always looking to improve. He believes that anyone can do what they set their mind to.
Unicorn College The author learned IT at the Unicorn College - a prestigious college providing education on IT and economics.
Previous article
Introduction to software testing in Java
All articles in this section
Testing in Java
Thumbnail
Next article
Java Testing - Hamcrest, JUnit TestRule and Best Practices
Activities (2)

 

 

Comments

To maintain the quality of discussion, we only allow registered members to comment. Sign in. If you're new, Sign up, it's free.

No one has commented yet - be the first!