Tag: testing

One data abstraction design approach

Our design approach should structured into four stages (typically applied cyclically): definition of the public interface, usage scenarios, test specification, and finally, implementation.

The design approach I describe is highly practical and low-level, involving the creation of model classes and the necessary TDD tests. This work is essential for transitioning smoothly into the development phase.

Public interface

This involves precisely identifying the public operations, understanding what they should expect or assume upon invocation (REQUIRES), identifying what they alter (MODIFIES), and specifying what outcomes they yield (EFFECTS).

For example, imagine we are designing a Microwave object. One of the operations to define in the public interface could be: “setting the cooking time.”

By specifying ‘REQUIRES,’ we define the method’s invariant, indicating the conditions that must be met for the method to function as expected. Any input value that does not fulfill these conditions falls outside our scope of responsibility for the resulting behavior, underscoring the importance of adhering to these preconditions for correct operation.

Flowchart

In the cases where the method has some complicated logic, we can add in the design a flowchart with the expected behavior.

This example is very simple, but it can be all the complex it needs the funcionality, and can have call to another objects or the same object methods.

Usage scenarios

It involves reviewing all the potential uses of your abstraction, which could lead to introducing new operations that need to be outlined.

What does that mean in practice?

Usage scenarios entail building a class with a main method to test the functionality. This hands-on approach helps determine if more methods are needed by directly assessing how well the current setup meets the intended use cases.

Test specification

This means writing a comprehensive and detailed test suite for each operation before implementing it.

Test-driven design involves writing tests after your specifications and implementing your code until the tests pass.

This testing is the best way to ensure that your implementation adheres closely to its intended specifications. The developer should continue refining the implementation until the test passes.

It is prudent to provide the development team with initially failing tests, as it aligns with the Test-Driven Development (TDD) methodology. This practice enhances the efficiency of the development process and ensures the functionality delivered is robust and aligns with the specified requirements.

Calculating Test Cases from Method Effects

We will create a test for each clause of the EFFECTS or we can create a test for each branch of the flowchart if it exists, for example:

Following the EFFECTS:

We will do a test for any branch: sunny and a person in the desert, sunny and a person in the city is not sunny.

We will combine these two branches for each possible input, one value inside ranges (negative, 0), (0, 15), (15, 25), (25, higger).

We will combine these two branches with the boundary limits, too: 0, 15, and 25.

Total: 3 branches x (4 + 3) => 21 cases to test.

Conclusion

To conclude, by providing the development team with a project that includes an abstract object, its public methods outlining the invariants, the object it modifies, and its effects, as well as flowcharts depicting the desired behavior and tests that must be passed before development is considered complete, we ensure that the development is delivered with higher quality and more swiftly.

Nuria

Robustness in Java: Exceptions and Assertions


In software development, robustness is indispensable for creating reliable and efficient applications. Java, one of the most widely used programming languages, offers various tools to help achieve this goal. Two of the most powerful are exceptions and assertions, though their ability to improve code quality is often underestimated. In this post, we’ll explore how these tools can be employed for error detection and as integral parts of a proactive strategy for building robust software.

Using Exceptions Beyond Error Handling

Traditionally, exceptions in Java have been seen as a way to handle errors during program execution. However, their utility goes beyond merely capturing unexpected errors. Exceptions can effectively manage situations that, while not necessarily catastrophic errors, represent non-ideal or unexpected conditions in business logic.

Practical Example: Adding Workers to Courses

Consider a method addWorkerToCourse(Worker worker, Course course) designed to enroll workers in training courses within a company. Instead of performing prior checks or returning a boolean value indicating the operation’s success, this method can throw an exception if the worker does not meet the course requirements:

public void addWorkerToCourse(Worker worker, Course course) throws RequirementsNotMetException {
    if (!meetsRequirements(worker, course)) {
        throw new RequirementsNotMetException("The worker does not meet the course requirements");
    }
    // Logic to add the worker to the course
}

This approach has several advantages. First, it simplifies error handling by centralizing validation within the method. Second, it enhances code readability, clarifying what conditions can prevent the operation from being successfully completed. Finally, it facilitates code reuse, as any method calling addWorkerToCourse must handle the exception, ensuring that necessary validations are not overlooked.

Assertions to Ensure Code Quality

Assertions are another underutilized tool in Java. Although they are executed only during development and testing (unless explicitly enabled in production), they are extremely useful for verifying that the program’s state is as expected at various execution points.

State Verification with Assertions

Imagine a scenario where we modify the value of a variable through a series of operations. We can use an assertion to verify that the final value is different from the initial one, helping us detect logical or state errors that might otherwise go unnoticed:

int valueBefore = getValue();
performOperationThatModifiesValue();
assert getValue() != valueBefore : "The value should have changed after performing the operation";

This type of check ensures that our assumptions about the program’s state are correct, crucial for preventing subtle, hard-to-track errors. Additionally, assertions during the development phase allow us to identify and correct problems before the code reaches production.

This is especially valuable in the case of private methods, where direct unit testing may not be feasible. Assertions provide a mechanism to validate the behavior of these methods, ensuring that they function as intended, even without direct test coverage.

The Importance of Testing Exception Throwing

A crucial part of writing robust software is ensuring exceptions are thrown under the appropriate circumstances. This involves properly handling exceptions when they occur and verifying through unit tests that these exceptions are thrown as we expect. These tests ensure that our error-handling logic works correctly under specific conditions.

Testing Exceptions with JUnit

Suppose we have a method that throws a RequirementsNotMetException if a worker does not meet the requirements to be added to a course, as discussed earlier. We want to write tests that verify this exception is correctly thrown.

Example: Test Fails if Exception Is Not Thrown

In this example, we expect the test to fail if the code inside the try block executes without throwing the expected exception:

@Test
public void testAddingWorkerToCourseWithUnmetRequirements() {
    try {
        Course course = new Course(...); // Configure course
        Worker worker = new Worker(...); // Configure worker without necessary requirements
        addWorkerToCourse(worker, course);
        fail("It was not expected to reach this line of code");
    } catch (RequirementsNotMetException e) {
        System.out.println("great");
    }
}

This test verifies that the method addWorkerToCourse throws a RequirementsNotMetException when trying to add a worker who does not meet the necessary requirements. If the exception is not thrown, the test will fail due to the call to fail().

Example: Test Fails if Exception Is Thrown

In this example, we expect the test to fail if the code inside the try block executes throwing the exception:

@Test
public void testAddingWorkerToCourseWithUnmetRequirements() {
    try {
        Course course = new Course(...); // Configure course
        Worker worker = new Worker(...); // Configure worker with necessary requirements
        addWorkerToCourse(worker, course);
    } catch (RequirementsNotMetException e) {
        fail("I was not expecting exception");
    }
}

Both examples illustrate how to effectively test the throwing of exceptions in Java, ensuring that our application behaves as expected under adverse conditions. This type of testing complements our error-handing strategy and is essential to building reliable and robust software.

Conclusion

Exceptions and assertions are essential tools in any Java developer’s arsenal. Used correctly, they help manage errors effectively and promote writing that is cleaner, more readable, and, above all, robust code. By integrating these practices into our projects, along with a rigorous approach to exception testing, we can significantly improve the quality and reliability of our code.