Development Environments

Software testing

Unit test design

  

Unit tests are an essential part of software testing, in which we must check if every unit of our application (class or set of functions) works as expected. In order to check this, we need to plan a strategy to define the set ot test cases as complete as possible.

There are three approaches to design these unit test cases:

Ejercicio1

1. White box tests

White box tests focus on the inner working of a program. This is the first tests that we must apply to a system, so that we can find basic shortcomings that are not related with user interface.

There are several types of white box tests, as we are going to see right now. We can apply one or many of the following approaches in order to complete these type of tests.

1.1. Basic path tests

This method was focused on determining the complexity of a piece of code, so that we use this complexity to establish how many execution paths can be achieved.

It relies on a principle that sets that every procedural design can be represented as a flow graph. The cyclomatic complexity of this graph determines the number of independent paths. Each one of these paths corresponds to a new set of sentences or a new condition. Let’s have a look a this short piece of code and the possible paths that can be run with it:

Ejercicio1

If we want to properly test this piece of code, we need to test the following paths:

Which correspond to the following tests:

So we should define 4 test cases, each one corresponding to an item of previous list, with the appropriate values for the input params a, b and c.

Exercise 1:

Design the white box test set for the following piece of code, using the Basic path test approach explained above. Determine the corresponding paths to be tested, and the test cases to test each path.

if (num1 > 10)
{
    if (num2 > 10)
        System.out.println("Both are greater");
    else
        System.out.println("First is greater");
} else {
    if (num2 > 10)
        System.out.println("Second is greater");
    else
        System.out.println("None is greater");
}

1.2. Condition tests

This method is similar to the previous one: it evaluates every possible path of the code, but it only focuses on the conditions of the code. Let’s have a look at this example:

public boolean isLeapYear(int year)
{
    boolean result = false;
    if(year % 4 == 0)
    {
        result = true;

        if(year % 100 == 0)
        {
            result = false;

            if(year % 400 == 0)
            {
                result=true;
            }
        }
    }
    return result;
}

Conditions:

From this set of conditions, we need to build the truth tables to check every possible combination:

N C1 C2 C3 Result
1 true true true true
2 true true false false
3 true false true true
4 true false false true
5 false true true false
6 false true false false
7 false false true false
8 false false false false

As we can see, cases 3 and 4 lead to the same result regardless of the value of C3. And the same thing happens with cases 5 to 8 (condition C1 determines the final result regardless of the other two conditions). So the tests needed for this function are:

N C1 C2 C3 Result
1 true true true true
2 true true false false
3 true false true true
4 false true true false

Again, we need to design 4 test cases associated to the 4 rows of previous table.

Exercise 2:

Repeat previous exercise using now the Condition tests approach.

1.3. Loop tests

This test evaluates the possible paths for loops. For every loop with n iterations, we must check if:

If we have any nested loop, we must start exploring the inner loops and then go to the outer ones.

For instance, let’s have a look at the following code that checks if a given number (previously entered by the user) is primer or not:

boolean result = true;
if (number == 0 || number == 1)
    result = false;
int i = 2;
while (i <= number / 2 && result)
{
    if (number % i == 0)
        result = false;
    else
        i++;
}

The loop is expected to run up to N = number / 2 - 1 times as much. From the loop test approach, we must design test cases in which:

So we could build this table for the test cases:

ID Name Data Expected result Actual result
U0 BasicCases 1 false  
U1 NoIterations 2 true  
U2 OneIteration 3 true  
U3 TwoIterations 9 false  
U4 MIterations 25 false  
U5 N-1Iterations 4 false  
U6 NIterations 23 true  

Exercise 3:

The following piece of code checks if a number has its digits in ascending order:

boolean result = true;
while (number >= 10 && result)
{
    int lastDigit = number % 10;
    number /= 10;
    int newLastDigit = number % 10;
    if (lastDigit < newLastDigit)
        result = false;
}

You are asked to design a test case table for every possible loop iteration, according to previous example.

2. Black box tests

These tests focus on the input and output of the application or module to be tested, and we don’t need to pay attention to the inner code of this module. There are also some different techniques that we can apply to these tests.

2.1. Equivalent partition

It consists in dividing the possible inputs of the application in groups called equivalence classes. Some input values will be valid inputs and some other will be not valid, so we must design test cases to check both valid and invalid equivalence classes.

For instance, if we have a method to determine the total amount of a sale, given the concept (string starting with letter), product amount (integer other than 0) and product price (double greater or equal than zero), the possible equivalence classes are:

Input condition Valid class Invalid class
Concept not empty starting with letter string=letter+* empty string OR string starting with number OR string starting with special character
Amount integer other than 0 amount other than 0 amount 0 OR not integer
Price double greater or equal than 0 price>=0 price<0 OR not numeric

Once we define the equivalence classes, we can design the test cases:

ID Name Preconditions Steps Data Expected result Actual result
U1 Valid SalesList object exists Enter valid classes for concept, amount and price concept=”screw”, amount=2,price=2 0, a new element is added  
U2 NotValidConcept1 SalesList object exists Enter empty string as concept concept=””, amount=2, price=2 -1, no element added  
U3 NotValidConcept2 SalesList object exists Enter string starting with number concept=”2screw”, cantidad=2, precio=2 -1, no element added  
U4 NotValidConcept3 SalesList object exists Enter string starting with special char concept=”@screw”, amount=2, price=2 -1, no element added  
U5 ValidAmount SalesList object exists Enter negative amount concept=”screw”, amount=-2, price=2 0, a new element is added  
U6 NotValidAmount SalesList object exists Enter amount of 0 concept=”screw”, amount=0, price=2 -1, no element added  
           

Exercise 4:

You have been asked to implement the tests for a class called SalesList, whose attribute is a HashMap<String,Double>. The string is the product description, and the number is the total amount of sales over this product. The class has the following methods:

Design the possible test cases for every method of the class. Regarding addSale method, you just have to complete the table shown in previous example. For getTotal and getAverage methods, you just need to set the preconditions to get the desired result, since they have no parameters.

2.2. Analysis of limit values

In order to design the test cases, we take into account input and ouput conditions:

For instance, let’s suppose that we are testing a function that checks the average of a list of marks that must contain between 3 and 9 marks. In this case, we should define these test cases to check the input values:

Regarding the output values, the average must be between 0 and 10, so we should also define cases for:

Depending on the internal code of the function to be tested, sometimes some of these cases are impossible to reach. For instance, if we make sure that marks are all values between 0 and 10, we will never get an average < 0 or > 10.

A possible test case table for this example could be this one:

ID Name Data Expected result Actual result
TC1 2Marks [2, 6] Error  
TC2 3Marks [4, 5, 6] 5  
TC3 4Marks [7, 7, 9, 9] 8  
TC4 8Marks [1, 2, 3, 4, 6, 7, 8, 9] 5  
TC5 9Marks [2, 3, 4, 5, 6, 7, 8, 9, 10] 6  
TC6 10Marks [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] Error  
TC7 Average-1 [-2, -1, -1] Error  
TC8 Average0 [0, 0, 0] 0  
TC9 Average1 [0, 1, 2] 1  
TC10 Average9 [8, 9, 10] 9  
TC11 Average10 [10, 10, 10, 10] 10  
TC12 Average11 [10, 11, 12] Error  

Exercise 5:

Let’s test a function that gets as input the day of a month (integer between 1 and 31) and a month number (integer between 1 and 12) and returns how many days are left in this month (an integer between 1 and 30, depending on the month).

int getDaysLeft(int dayOfMonth, int monthNumber) { ... }

Think of the possible test cases to cover all the limit values.