Functional Testing – Boundary Value Analysis, Equivalence and Category Partitioning

Functional Testing – Boundary Value Analysis, Equivalence and Category Partitioning

Functional testing is an essential part of software development that involves evaluating a system or its components to ensure that they meet the specified requirements. In this article, we will delve into three key techniques that are commonly used in functional testing: Equivalence Partitioning, Boundary Value Analysis, and Category Partitioning. These techniques are good for identifying and isolating issues in the system early on in the development process. In the next part of this article, we will take a look at how to test the application using the Cause-Effect Graph technique, another important aspect of functional testing.

These techniques are widely used to ensure that a software system is working correctly and is free of defects. We will explore the concepts behind each technique, and how they can be applied to different types of software systems. By the end of this article, you will have an overview understanding of how to effectively test your software using these powerful techniques.

Overview 🔍

Functional Testing is a type of software testing that validates the software system against the functional requirements and specifications. The purpose of functional tests is to test each function of the software application, provide the appropriate information and check production against functional requirements.

Key Features

  • The test data is generated according to the program specifications, the program structure plays no role;

  • The ideal specification type for functional testing includes preconditions and subsequent requirements;

  • Most functional methods are based on partitioning input data so that data belonging to the same partition will have (identical) properties associated with the specified behaviour;

Practically, due to time and budget considerations, it is not possible to perform exhausting testing for each set of test data, especially when there is a large pool of input combinations.

Equivalence Partitioning

The first type of Functional Testing is Equivalence Partitioning, also known as Equivalence Class Partitioning (ECP), and the basic idea is partitioning the domain of the problem (input data) in equivalence partitions or equivalence classes so that, from the specification point of view, the data in a class is treated in the same way. Since all values in a class have specified the same behaviour, it can be assumed that all values in a class will be processed in the same way, so it is enough to choose one value from each class.

In addition, the output field will be treated in the same way, and the resulting classes will be converted in reverse engineering into classes of the input field.

The equivalence classes must not overlap, so any overlapping classes must be broken down into separate classes. Once the classes have been identified, a value is chosen from each class. In addition, invalid data (which is outside of classes and is not processed by any class) may also be chosen. The choice of values in each class is arbitrary because it assumes that all values will be processed in the same way.

Simple Example 💻

This program checks whether a character is present within a string of 20 characters or less. It prompts the user to enter a string of a specific length (n) between 1 and 20 and then prompts the user to enter a character (c) to search for in the previously entered string. The program will then output the position of the first instance of the character in the string or a message indicating that it was not found. The user can then choose to search for another character by typing "y" or end the program by typing "n".

Let's write the specification:

import java.util.Scanner;

public class CharacterSearch {

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        String inputString;
        char searchChar;
        String searchAgain = "y";

        while (searchAgain.equals("y")) {
            // get the input string
            System.out.print("Enter a string of no more than 20 characters: ");
            inputString = scanner.nextLine();
            while(inputString.length()>20){
                System.out.println("String is too long, please enter a string of no more than 20 characters:");
                inputString = scanner.nextLine();
            }
            // get the character to search for
            System.out.print("Enter a character to search for: ");
            searchChar = scanner.nextLine().charAt(0);

            // search for the character in the string
            int charIndex = inputString.indexOf(searchChar);
            if (charIndex == -1) {
                System.out.println("The character '" + searchChar + "' was not found in the string '" + inputString + "'.");
            } else {
                System.out.println("The character '" + searchChar + "' was found at position " + (charIndex + 1) + " in the string '" + inputString + "'.");
            }

            // ask the user if they want to search for another character
            System.out.print("Search for another character? (y/n): ");
            searchAgain = scanner.nextLine();
        }
        System.out.println("Thank you for using the Character Search program!");
    }
}
 public int indexOfCharInString(String inputString, char searchChar) {
        if (inputString.length() > 20) {
            return -1;
        }
        return inputString.indexOf(searchChar);
    }

In this example, I've added a new method indexOfCharInString(inputString, searchChar) to the CharacterSearch class that takes the inputString and searchChar as inputs, and returns the index of the first occurrence of the searchChar in the inputString.

import org.junit.Test;
import static org.junit.Assert.*;

public class CharacterSearchTest {

    CharacterSearch cs = new CharacterSearch();

    @Test
    public void testIndexOfCharInString() {
        //Positive Test Case
        String inputString = "abcdefghijklmnopqrst";
        char searchChar = 'a';
        int expectedResult = 0;
        int result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        //Negative Test Case
        inputString = "abcdefghijklmnopqrstuvwxyz";
        searchChar = 'z';
        expectedResult = -1;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for max length of string
        inputString = "abcdefghijklmnopqrsz";
        searchChar = 'z';
        expectedResult = 19;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for min length of string
        inputString = "a";
        searchChar = 'a';
        expectedResult = 0;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for special characters
        inputString = "!@#abcdefghijklmnopq";
        searchChar = '#';
        expectedResult = 2;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);
    }
}

In the test class, CharacterSearchTest, I've created a test method testIndexOfCharInString() to test it using JUnit framework then I've created test cases for the positive, negative, max length of string, min length of string and special characters to test the method using the equivalence partitioning technique.

The test method first creates an instance of the CharacterSearch class, then it calls the indexOfCharInString(inputString, searchChar) method passing the inputString and searchChar as inputs, it then compares the expected result with the actual result, if they are equal the test will pass otherwise it will fail. This way we are testing the method for all possible inputs and also for the edge cases.

Including all of our conditions, we obtain the following domain:

Input data 👈

  • an integer n;

  • a string x;

  • the searching character c;

  • the option to search for a character (s), or not;

Input domain and constraints

  • n must be between 1 and 20, so there are 3 classes of equivalence:

    • N_1 = 1..20

    • N_2 = { n | n < 1}

    • N_3 = {n | n > 20}

  • The whole n determines the length of the string and nothing is stated about the different treatment of strings of different lengths so the second entry does not determine additional classes of equivalence;

  • c does not determine additional equivalence classes;

  • The option to search for a new character is binary, so 2 classes of equivalence are distinguished:

    • S_1 = { y } (yes, search for another character)

    • S_2 = { n } (no, do not search for another character)

Output data 👉

  • The position at which the character is found in the string;

  • A message that shows whether it was found or not;

We divide the output into 2 domain classes: One for the character in the string and one for the missing character.

  • C_1(x) = { c | c is in x}

  • C_2(x) = { c | c is not in x}

Equivalence Classes

At this point, we can extract equivalency classes. Equivalence classes are used to divide the input domain of a system or software into partitions of similar input values. An Equivalence class is a set of inputs or outputs that should behave in the same way for the same system or software. In this case, we extract the entire curriculum in the form of a combination of individual classes:

  • C_111 = { (n, x, c, s) | n \in N_1, |x| = n, c \in C_1(x), s \in S_1}

    For example, this case represents:

    1. n \in N_1: n is a number between 1 and 20;

    2. |x| = n: length of string x is equal with n;

    3. c \in C_1(x): character c is in string X;

    4. s \in S_1: yes, search for another character;

  • C_112 = { (n, x, c, s) | n \in N_1, |x| = n, c \in C_1(x), s \in S_2}

    For example, this case represents:

    1. n \in N_1: n is a number between 1 and 20;

    2. |x| = n: length of string x is equal with n;

    3. c \in C_1(x): character c is in string X;

    4. s \in S_1: no, do not search for another character;

  • C_121 = { (n, x, c, s) | n \in N_1, |x| = n, c \in C_2(x), s \in S_1}

  • C_122 = { (n, x, c, s) | n \in N_1, |x| = n, c \in C_2(x), s \in S_2}

  • C_2 = { (n, x, c, s) | n \in N_2}

  • C_3 = { (n, x, c, s) | n \in N_3}

Testing data

The test dataset is composed by selecting an entry value for each equivalence class. For example:

  • C_111 : (3, abc, a, y)

  • C_112 : (3, abc, a, n)

  • C_121 : (3, abc, d, y)

  • C_122 : (3, abc, d, n)

  • C_2 : (0, _, _, _)

  • C*_*3 : (25_, _, _)

The advantages

  1. Increased efficiency: By partitioning the input domain into equivalence classes, testers can create a smaller set of test cases that are representative of a larger set of inputs. This allows for more efficient testing, as fewer test cases need to be created and executed.

  2. Reduced costs: Because fewer test cases need to be created, the overall cost of testing is reduced. This can be especially beneficial for large or complex systems where the cost of testing can be significant.

  3. Improved coverage: Equivalence partitioning helps to ensure that the system or software is tested for a wide range of inputs, including the boundaries of the input domain. This helps to identify any issues or bugs that may occur at these boundaries, which can improve overall system quality.

  4. A better understanding of the system: By creating test cases that cover a wide range of inputs, testers gain a better understanding of how the system behaves under different conditions. This can be useful in identifying areas where the system may be particularly fragile or vulnerable.

  5. Better test case design: By identifying the equivalence classes the test engineer can design the test cases more effectively, and can cover the possible inputs and edge cases.

  6. Better maintenance: Equivalence partitioning can be useful in identifying areas of the system or software that may change frequently, which can help to ensure that these areas are tested thoroughly and that any changes are properly validated.

The disadvantages

While equivalence partitioning is a powerful technique for software testing, there are also some potential disadvantages to using it:

  1. Complexity: Identifying and defining the appropriate equivalence classes can be a complex task, especially for large or complex systems. Testers may need to have a deep understanding of the system or software to properly identify the appropriate equivalence classes.

  2. Limited coverage: While equivalence partitioning can help to improve the coverage of the input domain, it may not cover all possible inputs. Some inputs may fall outside of the defined equivalence classes, which can lead to potential defects or bugs going undetected.

  3. Subjectivity: The definition of equivalence classes is often based on the tester's subjective interpretation of the system or software's requirements. This can lead to different testers identifying different equivalence classes, which can make it difficult to ensure consistent testing across different teams or projects.

  4. Assumptions: Equivalence partitioning relies on certain assumptions about how the system or software behaves, which may not be accurate. This can lead to defects or bugs going undetected, or to test cases that are not valid for the system or software.

  5. Time-consuming: Identifying and defining the appropriate equivalence classes can be time-consuming, especially for large or complex systems. This can lead to delays in the testing process,

Boundary Value Analysis

Now let's talk about another cool testing technique...Boundary Value Analysis 😎

Boundary value analysis is a testing technique that focuses on testing the boundaries of the input domain of a system or software. The goal of boundary value analysis is to identify and test inputs that are at or near the boundaries of the input domain, as these inputs are more likely to cause errors or defects in the system or software.

The idea behind boundary value analysis is that the behaviour of a system or software can be different at the boundaries of the input domain than it is in the middle of the input domain. For example, a program that accepts an age as input and checks if the age is greater than 18 is likely to behave differently when the age is 18 than it would when the age is 19.

Boundary value analysis is often used in conjunction with equivalence partitioning to provide more comprehensive coverage of the input domain.

During the testing process, the tester will identify the boundaries of the input domain such as minimum and maximum values, and then create test cases that focus on these boundaries. For example, the tester would create test cases for the minimum and maximum values, and one test case for the value just below and just above the boundaries.

Example

import org.junit.Test;
import static org.junit.Assert.*;

public class CharacterSearchTest {

    CharacterSearch cs = new CharacterSearch();

      @Test
    public void testIndexOfCharInString2() {
        // Test case for the minimum length of string
        String inputString = "a";
        char searchChar = 'a';
        int expectedResult = 0;
        int result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for the maximum length of string
        inputString = "abcdefghijklmnopqrsz";
        searchChar = 'z';
        expectedResult = 19;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for the first character of the string
        inputString = "abcdefghijklmnopqrst";
        searchChar = 'a';
        expectedResult = 0;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for the last character of the string
        inputString = "abcdefghijklmnopqrsz";
        searchChar = 'z';
        expectedResult = 19;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for special characters
        inputString = "!@#abcdefghijklmnopq";
        searchChar = '#';
        expectedResult = 2;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);
    }
}

In this example, I've created test cases that focus on the boundaries of the input domain, such as the minimum and maximum length of the input string, the first and last characters of the string, and special characters.

By testing these specific cases, we can identify any issues that may occur at the boundaries of the input domain, which can improve overall system quality.

  • N_1 = 1..20

    • 1, 20 (boundary value) and a value from within;
  • N_2 = {n | n < 1}

    • 0 (boundary value) and a value from within;
  • N_3 = {n | n > 20}

    • 21 (boundary value) and a value from within;
  • C_1(x) = { c | c is in x}

    • On the first position in x, on the last position in x, inside of x;
  • C_2(x) = { c | c is not in x}

    • There are no clear borders, so no additional values appear;

Notes

The choice of boundaries is difficult and depends on the experience of the tester. For the choice of the border, the number of the appearance of c in row x: 0 appearances, 1 appearance and more than one appearance.

Category Partition

It is based on the two previous ones. It seeks to generate test data that “covers” the functionality of the system and maximizes the possibility of finding errors.

Steps

  • Breaks down functional specification into units (programs, functions, etc.) that can be tested separately;

  • For each unit, identify the environmental parameters and conditions (e.g. the state of the system at the time of execution) on which its behaviour depends;

  • Find the categories (important properties or characteristics) of each parameter or environmental condition;

  • Write the test specification. It consists of the list of categories and the list of alternatives for each category;

  • Create test cases by choosing a combination of alternatives from the test specification (each category contributes zero or an alternative);

  • Create test data by choosing a single value for each alternative;

Example

import org.junit.Test;
import static org.junit.Assert.*;

public class CharacterSearchTest {

    CharacterSearch cs = new CharacterSearch();

    @Test
    public void testIndexOfCharInString3() {
        // Test case for strings with length less than 20
        String inputString = "abc";
        char searchChar = 'b';
        int expectedResult = 1;
        int result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for strings with length equal to 20
        inputString = "abcdefghijklmnopqrsx";
        searchChar = 'x';
        expectedResult = 19;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);

        // Test case for strings with length greater than 20
        inputString = "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz";
        searchChar = 'z';
        expectedResult = -1;
        result = cs.indexOfCharInString(inputString, searchChar);
        assertEquals(expectedResult, result);
    }
}

In this example, I've created test cases that focus on different categories of the input domain. I've grouped the inputs into 3 categories, strings with a length less than 20, strings with a length equal to 20 and strings with a length greater than 20, and special characters.

By testing these specific categories, we can identify any issues that may occur with different lengths of the input string which can improve overall system quality. It's important to note that this is just an example and you should adjust the test cases according to your implementation and the requirements of the system.

  • Break down the specification into units: We have only one unit;

  • Identify parameters: n, x, c, s;

  • Find categories:

    • n: if it is in the valid range 1..20;

    • x: if it is of minimum, maximum or intermediate length;

    • c: if it occupies the first or last position or position inside x or does not appear in x;

    • s: if it is positive or negative;

  • Categorize each category into alternatives:

    • n: <0, 0, 1, 2..19, 20, 21, >21;

    • x: minimum, maximum or intermediate length;

    • c: the position is first, inside, or last or c does not appear in x;

    • s: y, n;

We are writing testing specifications:

  • For n

    • {n | n < 0}

    • 0

    • 1

    • 2..19

    • 20

    • 21

    • {n | n > 20}

  • For x

    • {x | |x| = 1}

    • {x | 1 < | x | < 20}

    • {x | |x| = 20}

  • For c

    • { c | c is in the first position in x}

    • { c | c is inside x}

    • { c | c he is in the last position in x}

    • { c | c not in x}

  • s

    • y

    • n

Test Cases

  • The test specification should result in 7 * 3 * 4 * 2 = 168 test cases;

  • Some combinations of alternatives make no sense and can be eliminated;

  • The alternatives will only be combined if the selection conditions are satisfied;

  • For example, the number of test cases is reduced to 24;

Examples of test cases

  • n1

  • n2

  • n3x1c1s1

  • n3x1c1s2

  • n3x1c4s1

  • n3x1c4s2

  • n4x2c1s1

  • n4x2c1s2

  • n4x2c2s1

  • n4x2c2s2

  • n4x2c3s1

  • n4x2c3s2

  • n4x2c4s1

  • n4x2c4s2

  • n5x3c1s1

  • n5x3c1s2

  • n5x3c2s1

  • n5x3c2s2

  • n5x3c3s1

  • n5x3c3s2

  • n5x3c4s1

  • n5x3c4s2

  • n6

  • n7

Advantages and disadvantages

The starting steps (identification of environmental parameters and conditions as well as categories) are not well-defined and are based on the experience of the tester. On the other hand, once these steps have been passed, the application of the method is very clear.

Category Partitioning is more clearly defined than previous functional methods and can produce more comprehensive test data that tests additional functionality. On the other hand, due to the combinatorial explosion, very large test data can result.

Conclusion

In conclusion, functional testing is an essential part of the software development process. Equivalence Partitioning, Boundary Value Analysis, and Category Partition are some of the techniques that can be used to ensure that a system functions correctly. By using these techniques, we can identify and isolate issues early on in the development process which can save time and resources in the long run.

Equivalence Partitioning helps to divide the input domain into smaller, more manageable chunks, making it easier to identify test cases. Equivalence Classes are the specific group of inputs that are used to test a system. By using Equivalence Classes, we can ensure that our test cases are representative of the entire input domain.

Boundary Value Analysis is a technique that focuses on the limits of the input domain. By testing the boundaries of the input domain, we can identify and isolate issues that may occur at the limits of the system.

Category Partition is a technique that groups inputs into specific categories. By testing each category separately, we can identify and isolate issues that may occur within a specific category of inputs.

In this article, I have shown you how to use these techniques to test a program that checks if a character is in a string of no more than 20 characters. I hope that you found the information in this article helpful and that you now have a better understanding of how to use these techniques to improve the quality of your software.

In this article, we have covered the first aspect of Functional Testing. In the following instalment, we will delve into the topic Cause Effect Graphing.

Those interested in learning more about Functional Testing can read more in the presentation of Aditya P. Mathur about the Foundations of Software Testing, and an additional in-depth example can be found on my GitHub project.

Reference 📙

[1] Gist One: https://gist.github.com/mihaichris/f60105f9617e651ece7531b0ffbf543e

[2] GitHub Project: https://github.com/mihaichris/kata-ohce

[3] Aditya P. Mathur. Foundations of Software Testing, Pearson Education 2008. Chapter 3: Domain Partitioning