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
Userobject 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.