Module 04: Quality Assurance, Part 05: Testing Strategies

Introduction to Unit Testing

Unit testing involves the examination of individual parts of the code to ensure their functionality. The primary units tested are often classes in object-oriented programs or functions/operations in procedural programming. The goal is to cover as many potential defects as possible during the testing process, ensuring high reliability and performance of the software.

Understanding the Unit of Testing

Classes and Attributes

A class is typically defined with attributes (e.g., ID, name) and operations (e.g., do A, do B). For example, consider a class User which has attributes like userID, username, and email. Each of these attributes must be tested for their ability to be set, modified, and read. It is important to note that certain attributes might be restricted from modification (like userID), and therefore should not be tested for set capabilities.

Example Test Cases:

  • Test Case for Setting Username: Check if the username can be set correctly by running a test that initializes a User object and sets the username with valid input.

Operations

Each operation within a class should be called and tested with diverse input parameters. Tests should cover:

  • Expected Inputs (Valid Cases): i.e., calling an operation that computes a user's age based on their birth year with valid data.

  • Unexpected or Erroneous Inputs: For example, calling the operation with null, undefined, or negative values should be tested to ensure proper error handling.

State Testing of Classes

Classes may have different states that can change over time. For instance, a LightSwitch class can be either on or off. Tests should vary the states to ensure all possible conditions are assessed. Example Test Case: Change the state of the LightSwitch from off to on, and check if the output state reflects this change accurately.

Heuristics for Effective Testing

Heuristics help identify common pitfalls in programming that can lead to defects:

Zero or One-length Sequences

Test cases where inputs are empty or have only one element to identify potential oversights. For example, testing a function that calculates averages on an empty array should result in a controlled error or a specific return value (like null).

Buffer Overflows

Tests should include inputs that exceed predefined limits, like trying to store 101 elements in an array that is limited to 100. This would help in identifying how the code handles such overflows, potentially revealing vulnerabilities.

Edge Cases with Range Limits

Use maximum/minimum values for data types to check for issues like overflow or unexpected behavior. For example, testing a function that accepts integers should encompass both the maximum integer value and one more than that to see if the function correctly raises an overflow error.

Handling Undefined Values

Incorporate null and undefined values in the tests as these can behave differently in various programming languages. For instance, a function that performs arithmetic operations should be robust to handle undefined or null arguments and return a predefined error message.

Importance of Basic Heuristics

Despite seeming trivial, these heuristics are potent tools in programming. Even experienced programmers can overlook basic principles, thus it's crucial not to underestimate their importance in achieving robust code.

Concept of Partition Testing

Partition Testing allows input to be categorized into distinct classes based on expected behavior: Example classes could be:

  • Negative integers (-10 to 0)

  • Positive integers (1 to 100)

  • Integers above a threshold (over 100) Instead of testing every integer, select typical values from each class (e.g., -5, 10, 200) and check expected outcomes versus actual outcomes.

Boundary Testing

Focus on testing at the boundaries of input classes (e.g., -1, 0, 1, 100, 101) to catch common logical errors during implementation. For example, if a function is supposed to count items from 1 to 100, testing its performance at 0 and 101 will verify accurate boundary conditions.

Conclusion: Strategies for Effective Unit Testing

Utilize the described strategies and heuristics when writing unit tests to ensure extensive case coverage and identify potential defects efficiently. These strategies help maintain code quality and ensure that software is reliable and performs as expected. Upcoming discussions will delve into the concept of code coverage and its significance.