Skip to content
Image with logo, providing a link to the home page
  • United Stated of America flag, representing the option for the English language.
  • Bandeira do Brasil, simbolizando a opção pelo idioma Português do Brasil.

Learn Programming: Tests and Debugging

Example of using bitwise operations in four programming languages: Python, Lua, GDScript and JavaScript.

Image credits: Image created by the author using the program Spectacle.

Requirements

In the introduction to development environments, I have mentioned Python, Lua and JavaScript as good choices of programming languages for beginners. Later, I have commented about GDScript as an option for people who want to program digital games or simulations. For the introductory programming activities, you will need, at least, a development environment configured for one of the previous languages.

If you wish to try programming without configuring an environment, you can use of the online editors that I have created:

However, they do not provide all features offered by interpreters for the languages. Thus, sooner or later, you will need to set up a development environment. If you need to configure one, you can refer to the following resources.

Thus, if you have an Integrated Development Environment (IDE), or a combination of text editor and an interpreter, you are ready to start. The following example assumes that you know how to run code in your chosen language, as presented in the configuration pages.

If you want to use another language, the introduction provides links for configure development environments for the C, C++, Java, LISP, Prolog, and SQL (with SQLite) languages. In many languages, it suffices to follow the models from the experimentation section to modify syntax, commands and functions from the code blocks. C and C++ are exceptions, for they require pointers access the memory.

The Root of the Issue

Almost every minimally complex program has issues. Software problems can be categorized as defects, errors, or failures; bugs, in the popular language.

Over the previous topics, some resources and techniques to debug have been mentioned. As errors are an intrinsic part of programming, it is useful knowing techniques to identify and fix them. There are tools to debug and test software. Debuggers, test frameworks, continuous integration, and source-control management systems are examples of tools that can increase the maintainability of projects.

To start, there are different types and hierarchies of bugs in software. For instance, bugs can result from:

  1. Problems in the project or on its implementation. In this case, the error is in the code that implements a feature;
  2. Problems in the use of code. In this case, the error is due to an incorrect call or use of correct code. As it was commented in Libraries, the use of a library presupposes a contract If the assumptions are violated, there are no guarantees that the call of a subroutine or the use of record will be valid;
  3. Problems that are external to the program. For instance, the program may try using a file that no longer exists or access an online resource that is unavailable.

In the first case, the bug exists in the project or library. To eliminate it, one must identify where and why it happens. In other words, she/he must perform an investigative work (acting as a detective) to examine the incorrect result (or crashing), and find the root of the issue. If she/he understands the root cause, she/he can fix it. The correction is often called a patch, in analogy to a medical patch.

In the second case, the bug is due to a misuse of an Application Programming Interfaces (API). The order of the parameters may be wrong, a value may have been typed incorrectly, or a wrong variable has been passed to the subroutine. In static typed languages, the compiler tends to warn about incorrect types. In dynamic typed languages, this does not happen. Therefore, this situation tends to be more usual in dynamic typing, especially when used by beginners and/or when one uses a library for a domain that she/he is not very experienced at.

In the two previous cases, the bug may be fatal. With luck, it will make the program crash during development, revealing the problem before end-users interacting with the system. This tends to be the best case scenario, because the issue can be fixed before it affects people who will use the source code or the program.

The third case is different from the first two. There is code that can be used correctly, and yet it may fail due to external factors. Typical examples are implementations that manipulate files, access databases, and/or use network resources. In general, although it is not possibly to guarantee that a call will work, one can know beforehand some (or all) adverse scenarios. Thus, it is possible to hand the adverse scenario and keep the program working. To do this, the called code must return the identified error to enable the caller can handle it suitably.

Robust programs depends on good error handling. Besides resulting in a more stable system, error handling can make the system safer by benefit from defensive programming. This is an important differential between a programming beginner and a professional. Therefore, it is worth starting this topic with some strategies to handle errors.

Error Handling

There are two common approaches to communicate a developer that the use or a call to a code has failed: returning an error code or throwing an exception. This allows the implementation that, for instance, has called a subroutine to handle the identified error the best way possible.

In fact, a good program tries to handle errors in the best possible way without compromising the operation, stability, and the responsivity of the system. Whenever possible, the program returns to is normal execution flow. In cases that is inevitable to exit the process, the program should aim to end the process graciously, with an informative error message and without crashing.

The examples will write the errors to the standard error output stderr, as presented in Files and Serialization (Marshalling). Some command line interpreters and IDE may provide custom formatting when writing errors. Beside errors, messages may be also formatted as warning, as discussed in the same topic.

Error Codes

A traditional way to inform whether a call has succeeded or failed consists in returning a special value. In the simplest way, this can be performed by returning an impossible or invalid value for the performed operation. In fact, libraries often return values such as -1 and null (or similar) to inform that the call was not successful. In functions implemented this way, it is essential to verify the obtained value before using it, to ensure that the result is valid. Therefore, one should consult the documentation of a subroutine before using it, to learn about the possible adverse effects.

However, it is not always possible to return an arbitrary value as an error code. In some cases, every possible value of a data type is potentially valid as the result of an operation. In others, it can be necessary to distinguish between an invalid or null value from an error returned with an invalid or null value. For instance, is null the calculated value or was null provided as an error code? The return can be ambiguous.

A different strategy consists of always providing an error code to inform whether the call has succeeded or failed. In the simple way, this can be achieved by returning a logic (boolean) value. The function call has succeeded is it returned True; if it is False, an issue has certainly happened.

In languages that allow a function to return multiple values, such as Python and Lua, this can be easily implemented.

import sys

def divide(dividend, divisor):
    if (divisor == 0):
        return False, None

    return True, dividend / divisor

for divisor in [2, 0]:
    success, result = divide(1.0, divisor)
    if (success):
        print(result)
    else:
        print("Division by zero.", file=sys.stderr)
function divide(dividend, divisor)
    if (divisor == 0) then
        return false, null
    end

    return true, dividend / divisor
end

for _, divisor in ipairs({2, 0}) do
    local success, result = divide(1.0, divisor)
    if (success) then
        print(result)
    else
        io.stderr:write("Division by zero.\n")
    end
end

In languages that do not allow it, an alternative to work around the limitation consists of returning a record or dictionary.

function divide(dividend, divisor) {
    if (divisor === 0) {
        return {
            success: false,
            result: undefined
        }
    }

    return {
        success: true,
        result: dividend / divisor
    }
}

for (let divisor of [2, 0]) {
    let result = divide(1.0, divisor)
    if (result.success) {
        console.log(result.result)
    } else {
        console.error("Division by zero.")
    }
}
import sys

class Result:
    def __init__(self, success, result = None):
        self.success = success
        self.result = result

def divide(dividend, divisor):
    if (divisor == 0):
        return Result(False)

    return Result(True, dividend / divisor)

for divisor in [2, 0]:
    result = divide(1.0, divisor)
    if (result.success):
        print(result.result)
    else:
        print("Division by zero.", file=sys.stderr)
function divide(dividend, divisor)
    if (divisor == 0) then
        return {
            success = false,
            result = null
        }
    end

    return {
        success = true,
        result = dividend / divisor
    }
end

for _, divisor in ipairs({2, 0}) do
    local result = divide(1.0, divisor)
    if (result.success) then
        print(result.result)
    else
        io.stderr:write("Division by zero.\n")
    end
end
extends Node

class Result:
    var success
    var result

    func _init(success, result = null):
        self.success = success
        self.result = result

func divide(dividend, divisor):
    if (divisor == 0):
        return Result.new(false)

    return Result.new(true, dividend / divisor)

func _ready():
    for divisor in [2, 0]:
        var result = divide(1.0, divisor)
        if (result.success):
            print(result.result)
        else:
            printerr("Division by zero.")

The implementations alternate between records and dictionaries; both options are valid. For more robust implementations, one could check the type of the variable. If it was not numeric, an error could be returned.

In the case of the hypothetical robust version, it can be worth defining the type of cause of the error. To do this, the implementation can return an integer value (or value from an enumeration) with a more granular error code. This is performed, for instance, by Godot Engine, which defines OK with value 0 and several error codes with the prefix ERR_ (documentation); for instance, ERR_OUT_OF_MEMORY is the error returned when there is no available memory for an allocation.

const OK = 0
// Generic error.
const ERROR = 1
// Specific errors.
const ERROR_DIVISION_BY_ZERO = 2
const ERROR_INVALID_TYPE = 3

function divide(dividend, divisor) {
    if ((typeof(dividend) !== "number") || (typeof(divisor) !== "number")) {
        return {
            error: ERROR_INVALID_TYPE,
            result: undefined
        }
    }

    if (divisor === 0) {
        return {
            error: ERROR_DIVISION_BY_ZERO,
            result: undefined
        }
    }

    return {
        error: OK,
        result: dividend / divisor
    }
}

for (let divisor of [2, 0, "Franco"]) {
    let result = divide(1.0, divisor)
    if (result.error === OK) {
        console.log(result.result)
    } else if (result.error === ERROR_DIVISION_BY_ZERO) {
        console.error("Division by zero.")
    } else if (result.error === ERROR_INVALID_TYPE) {
        console.error("Invalid type passed as a parameter.")
    } else {
        console.error("Another error.")
    }
}
import sys
from typing import Final

OK: Final = 0
# Generic error.
ERROR: Final = 1
# Specific errors.
ERROR_DIVISION_BY_ZERO: Final = 2
ERROR_INVALID_TYPE: Final = 3

class Result:
    def __init__(self, error, result = None):
        self.error = error
        self.result = result

def divide(dividend, divisor):
    if ((not isinstance(dividend, (int, float))) or (not isinstance(divisor, (int, float)))):
        return Result(ERROR_INVALID_TYPE)

    if (divisor == 0):
        return Result(ERROR_DIVISION_BY_ZERO)

    return Result(OK, dividend / divisor)

for divisor in [2, 0, "Franco"]:
    result = divide(1.0, divisor)
    if (result.error == OK):
        print(result.result)
    elif (result.error == ERROR_DIVISION_BY_ZERO):
        print("Division by zero.", file=sys.stderr)
    elif (result.error == ERROR_INVALID_TYPE):
        print("Invalid type passed as a parameter.", file=sys.stderr)
    else:
        print("Another error.", file=sys.stderr)
local OK <const> = 0
-- Generic error.
local ERROR <const> = 1
-- Specific errors.
local ERROR_DIVISION_BY_ZERO <const> = 2
local ERROR_INVALID_TYPE <const> = 3

function divide(dividend, divisor)
    if ((not (type(dividend) == "number")) or (not (type(divisor) == "number"))) then
        return {
            error = ERROR_INVALID_TYPE,
            result = null
        }
    end

    if (divisor == 0) then
        return {
            error = ERROR_DIVISION_BY_ZERO,
            result = null
        }
    end

    return {
        error = OK,
        result = dividend / divisor
    }
end

for _, divisor in ipairs({2, 0, "Franco"}) do
    local result = divide(1.0, divisor)
    if (result.error == OK) then
        print(result.result)
    elseif (result.error == ERROR_DIVISION_BY_ZERO) then
        print("Division by zero.")
    elseif (result.error == ERROR_INVALID_TYPE) then
        io.stderr:write("Invalid type passed as a parameter.\n")
    else
        io.stderr:write("Another error.\n")
    end
end
extends Node

const OK = 0
# Generic error.
const ERROR = 1
# Specific errors.
const ERROR_DIVISION_BY_ZERO = 2
const ERROR_INVALID_TYPE = 3

class Result:
    var error
    var result

    func _init(error, result = null):
        self.error = error
        self.result = result

func divide(dividend, divisor):
    if ((not (typeof(dividend) == TYPE_INT or typeof(dividend) == TYPE_REAL)) or
        (not (typeof(divisor) == TYPE_INT or typeof(divisor) == TYPE_REAL))):
        return Result.new(ERROR_INVALID_TYPE)

    if (divisor == 0):
        return Result.new(ERROR_DIVISION_BY_ZERO)

    return Result.new(OK, dividend / divisor)

func _ready():
    for divisor in [2, 0, "Franco"]:
        var result = divide(1.0, divisor)
        if (result.error == OK):
            print(result.result)
        elif (result.error == ERROR_DIVISION_BY_ZERO):
            printerr("Division by zero.")
        elif (result.error == ERROR_INVALID_TYPE):
            printerr("Invalid type passed as a parameter.")
        else:
            printerr("Another error.")

Regardless of the case for the returned value, the use of the function becomes slightly less convenient, because the returned value cannot be used directly to check for an error.

In programming languages that allow passing parameters of any type by reference (such as C and C++), a different implementation can return a logic value, which the parameter is modified by reference in case of success. Although this cannot be done for primitive types in some programming languages, this is often possible for some composite types (such as arrays and dictionaries).

// Linux: gcc main.c && ./a.out

#include <stdio.h>

#define FALSE 0
#define TRUE 1

int divide(float* result, float dividend, float divisor) {
    if (divisor == 0) {
        return FALSE;
    }

    *result = dividend / divisor;
    return TRUE;
}

int main() {
    const float divisors[] = {2.0, 0.0};
    for (int index = 0; index < 2; ++index) {
        float divisor = divisors[index];
        float result;
        if (divide(&result, 1.0, divisor)) {
            printf("%f\n", result);
        } else {
            printf("Division by zero.\n");
        }
    }

    return 0;
}
// Linux: g++ main.cpp && ./a.out

#include <array>
#include <iostream>

bool divide(float& result, float dividend, float divisor) {
    if (divisor == 0) {
        return false;
    }

    result = dividend / divisor;
    return true;
}

int main() {
    const std::array<float, 2> divisors{2.0, 0.0};
    for (float divisor: divisors) {
        float result;
        if (divide(result, 1.0, divisor)) {
            std::cout << result << std::endl;
        } else {
            std::cout << "Division by zero." << std::endl;
        }
    }

    return 0;
}
function divide(result, dividend, divisor) {
    if (divisor === 0) {
        return false
    }

    result[0] = dividend / divisor
    return true
}

for (let divisor of [2, 0]) {
    let result = [0]
    if (divide(result, 1.0, divisor)) {
        console.log(result[0])
    } else {
        console.error("Division by zero.")
    }
}
import sys

def divide(result, dividend, divisor):
    if (divisor == 0):
        return False

    result[0] = dividend / divisor
    return True

for divisor in [2, 0]:
    result = [0]
    if (divide(result, 1.0, divisor)):
        print(result[0])
    else:
        print("Division by zero.", file=sys.stderr)
function divide(result, dividend, divisor)
    if (divisor == 0) then
        return false
    end

    result[0] = dividend / divisor
    return true
end

for _, divisor in ipairs({2, 0}) do
    local result = {0}
    if (divide(result, 1.0, divisor)) then
        print(result[0])
    else
        io.stderr:write("Division by zero.\n")
    end
end
extends Node

func divide(result, dividend, divisor):
    if (divisor == 0):
        return false

    result[0] = dividend / divisor
    return true

func _ready():
    for divisor in [2, 0]:
        var result = [0]
        if (divide(result, 1.0, divisor)):
            print(result[0])
        else:
            printerr("Division by zero.")

Evidently, the use of the previous approach for primitive types in languages such as JavaScript, Python, Lua and GDScript is not recommendable, nor idiomatic. However, for operations with types passed by reference, it is valid.

Exceptions

The second approach to inform about errors consists of using exceptions. An exception is thrown by the code that identifies the error. Any part of the code that exists in previous levels of the execution stack can handle it. In potential, the handler can throw the exception again, enabling other parts to continue the handling.

JavaScript (documentation; hierarchy) and Python are languages supporting exceptions (documentation).

function divide(dividend, divisor) {
    if (divisor === 0) {
        throw "Division by zero."
    }

    return (dividend / divisor)
}

for (let divisor of [2, 0]) {
    try {
        let result = divide(1.0, divisor)
        console.log(result)
    } catch (exception) {
        console.error(exception)
    }
}
import sys

def divide(dividend, divisor):
    if (divisor == 0):
        raise Exception("Division by zero")

    return (dividend / divisor)

for divisor in [2, 0]:
    try:
        result = divide(1.0, divisor)
        print(result)
    except Exception as exception:
        print(exception, file=sys.stderr)

When one works with exception, it is not recommended using the base Exception class or similar. Normally it is better to follow the hierarchies defined by the language or create one's own custom exceptions. This allows handling exceptions with greater granularity, depending on the kind of error. The previous links to the documentation provide basic hierarchies for JavaScript and Python.

class DivisionByZeroError extends RangeError {
    constructor(message) {
        super(message)
    }
}

function divide(dividend, divisor) {
    if ((typeof(dividend) !== "number") || (typeof(divisor) !== "number")) {
        throw new TypeError("Invalid type for parameter.")
    }

    if (divisor === 0) {
        throw new DivisionByZeroError("Attempt to divide by zero.")
    }

    return (dividend / divisor)
}

for (let divisor of [2, 0, "Franco"]) {
    try {
        let result = divide(1.0, divisor)
        console.log(result)
    } catch (exception) {
        // console.trace()
        if (exception instanceof DivisionByZeroError) {
            console.error(exception)
        } else if (exception instanceof TypeError) {
            console.error(exception)
        } else {
            console.error(exception)
        }
    }
}
import sys
import traceback

def divide(dividend, divisor):
    if ((not isinstance(dividend, (int, float))) or (not isinstance(divisor, (int, float)))):
        raise TypeError("Invalid type for parameter.")

    if (divisor == 0):
        raise ZeroDivisionError("Attempt to divide by zero.")

    return (dividend / divisor)

for divisor in [2, 0, "Franco"]:
    try:
        result = divide(1.0, divisor)
        print(result)
    except ZeroDivisionError as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())
    except TypeError as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())
    except Exception as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())

When one uses exception, it can be useful to write the call stack as a traceback. In Python, the module Traceback (documentation) allows writing the call stack. In JavaScript, it can use console.trace() (documentation), although console.error() also does it by default.

Some exception implementation provide a third statement called finally. The code in finally finally is executed both when an exception is thrown and when the code works normally. This can be useful, for instance, when one wants to close a file regardless of the result of the called code.

class DivisionByZeroError extends RangeError {
    constructor(message) {
        super(message)
    }
}

function divide(dividend, divisor) {
    if ((typeof(dividend) !== "number") || (typeof(divisor) !== "number")) {
        throw new TypeError("Invalid type for parameter.")
    }

    if (divisor === 0) {
        throw new DivisionByZeroError("Attempt to divide by zero.")
    }

    return (dividend / divisor)
}

for (let divisor of [2, 0, "Franco"]) {
    try {
        let result = divide(1.0, divisor)
        console.log(result)
    } catch (exception) {
        // console.trace()
        if (exception instanceof DivisionByZeroError) {
            console.error(exception)
        } else if (exception instanceof TypeError) {
            console.error(exception)
        } else {
            console.error(exception)
        }
    } finally {
        console.log("Finally")
    }
}
import sys
import traceback

def divide(dividend, divisor):
    if ((not isinstance(dividend, (int, float))) or (not isinstance(divisor, (int, float)))):
        raise TypeError("Invalid type for parameter.")

    if (divisor == 0):
        raise ZeroDivisionError("Attempt to divide by zero.")

    return (dividend / divisor)

for divisor in [2, 0, "Franco"]:
    try:
        result = divide(1.0, divisor)
        print(result)
    except ZeroDivisionError as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())
    except TypeError as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())
    except Exception as exception:
        print(exception, file=sys.stderr)
        print(traceback.format_exc())
    finally:
        print("Finally")

The use of exception can make the implementation cleaner than using error codes. Nevertheless, in general the choice between the approaches can depend on personal preferences of requirements for the project. For instance, the use of exception can cause undesirable overhead in systems on which performance is fundamental or the memory is limited. Overhead refers to computational costs that are required to run a code, though that could be avoided with different implementations (though potentially less practical, and more complex and restricted). In other words, overhead is commonly a synonym of wasted resources.

Traceback in Lua and GDScript

Lua and GDScript do support exceptions. However, it is still possible to write a traceback. This has been previously done, for instance, in Files and Serialization (Marshalling).

function divide(dividend, divisor)
    if (divisor == 0) then
        error("Division by zero.")
    end

    return (dividend / divisor)
end

for _, divisor in ipairs({2, 0}) do
    local result = divide(1.0, divisor)
    print(result)
end
extends Node

func divide(dividend, divisor):
    if (divisor == 0):
        push_error("Division by zero")
        assert(false)

    return (dividend / divisor)

func _ready():
    for divisor in [2, 0]:
        var result = divide(1.0, divisor)
        print(result)

In the case of Lua, error() allows changing where the call stack is informed.

function divide(dividend, divisor)
    if (divisor == 0) then
        error("Division by zero.", 2)
    end

    return (dividend / divisor)
end

for _, divisor in ipairs({2, 0}) do
    local result = divide(1.0, divisor)
    print(result)
end

The value 2 in error() makes the interpreter show a different point of the call stack as the starting call. With the value 0, the call does not write the line that has originated the error.

Logging: Recording Errors

For projects under development, messages are often written in a terminal or console. Subroutines such as console.log() and print() are convenient for this purpose.

For programs distributed to end-users, it can be convenient to save messages to a file instead of a writing them in a terminal. As it has been mentioned in Files and Serialization (Marshalling), text files are frequently used for logging. This allows persisting errors, warning and/or other relevant information to a file.

In some programming languages, one can redirect stdout (standard output) and stderr (standard error) to a file. This can be performed in two ways:

  1. In Command Line Input, using features such as > file.txt to redirect the output. > file.txt redirects the standard output, which can also be performed using 1> file.txt. To redirect the standard error, one must use 2>. To redirect both, one can use &> file.txt. Another possibility us redirecting stderr to stdout, then stdout to a file. This can be performed as 2>&1 > file.txt;
  2. With resources provided by the standard library of the language. Some implementations allow choosing a file instead of using the default ones.

The first approach is external to the program. The second is internal to the program; therefore, it can be defined in the source code.

// node script.mjs

import * as fs from "fs"

let output = fs.createWriteStream("output.txt")
let error_output = fs.createWriteStream("error.txt")

// For simple uses:
// process.stdout.write = output.write.bind(output)
// process.stderr.write = error_output.write.bind(error_output)
// console.log("Olá, meu nome é Franco!")
// console.error("Error: My name is not Franco?")

// For a more sophisticated solution:
import { Console } from "console"
console = new Console(output, error_output)
console.log("Olá, meu nome é Franco!", 1, 2, 3, true)
console.error("Error: My name is not Franco?")

output.close()
error_output.close()
import sys

sys.stdout = open("output.txt", "w")
sys.stderr = open("error.txt", "w")

print("Olá, meu nome é Franco!")
print("Error: My name is not Franco?", file=sys.stderr)

sys.stdout.close()
sys.stderr.close()
io.stdout = io.open("output.txt", "w")
io.stderr = io.open("error.txt", "w")

function print(...)
    local argumentos = {...}
    for index, argumento in ipairs(argumentos) do
        if (index > 1) then
            io.stdout:write("\t")
        end

        io.stdout:write(argumento)
    end

    io.stdout:write("\n")
    io.stdout:flush()
end

print("Olá, meu nome é Franco!", 1, 2, 3, "foo")
io.stdout:write("Olá, meu nome é Franco!")
io.stderr:write("Error: My name is not Franco?")

io.stdout:close()
io.stderr:close()

The example writes Hello, my name is Franco! in Portuguese; it has not been translated to highlight the encoding in a later example. With the configuration, the output will be written in the files output.txt and error.txt, instead of the in the console (terminal).

In JavaScript, the solution uses Node.js for console or terminal use. The solution uses the library fs (documentation) and, optionally, console (documentation) as well. In the case of JavaScript for browsers, an alternative is using the embedded console. If one right-clicks the output area, one of the options allows saving the messages.

In Lua, the implementation defines a variadic (or var arg) function to write each argument received by print() to the redirected output. A variadic function can have a variable number of parameters, hence the name var arg (documentation). Subroutines such print() are typically variadic; this is the reason why it is possible to write print("Hello") e print("Hello", " my ", "name is ", "Franco").

In GDScript, the output can be configured in the editor, as a project setting (documentation). The option is in Project, Project Settings..., General, Logging. To enable file output, one should mark Enable File Logging and choose a file in Log Path. The file will be generated in user://; to find it, it is worth consulting the documentation, because the directory varies according the the operating system (documentation):

  • Windows: %APPDATA%/ProjectName/;
  • Linux and macOS: ~/.local/share/godot/app_userdata/ProjectName/.

ProjectName is the name defined for the project in Project, Project Settings..., Application, Config, Name.

In programming languages supporting high-order functions, it is also possible to redefine print() and console.log().

Automated Tests

Two approaches for automated source code tests have been commented over Learn Programming: assertions and unit test.

Assertions

The use of assertions has been commented in Subroutines (Functions and Procedures) and in Records (Structs).

let value = 1
console.assert(value % 2 === 0, "value must be even.")
value = 1
assert value % 2 == 0, "value must be even."
local value = 1
assert(value % 2 == 0, "value must be even.")
extends Node

func _ready():
    var value = 1
    assert(value % 2 == 0, "value must be even.")

The asserted condition must result True, because it is an assumption about the verified value. A False results means that the provided value does not correspond to the expected one; therefore, it represents an invalid use of the API.

As it has been commented in the cited topics, a typical use of assert() only applies to development activities. In optimized versions, assert() is usually disabled. Thus, run-time error checking must use Conditional (or selection) structures instead.

Unit Test

Unit Test has been commented in Subroutines (Functions and Procedures) and in Libraries. The example from Libraries is practical, using a test framework in GDScript.

Programming languages can provide unit test libraries as part of the standard library and/or as external libraries.

For instance, Python provides the module unittest (documentation) as part its standard library.

JavaScript does not provide a standard library for unit test. External library may support a browser and/or a command line interpreter (such as Node.js). Two popular frameworks for unit test are Mocha and Jest.

Like JavaScript, Lua does not provide a standard unit test library, although there are many options as external libraries. As usual, lua-users.org list some options.

Code Coverage

When one works with unit test, an important metric is code coverage. The coverage is the ration of tested lines of codes by the total lines codes in the project. Evidently, the ideal coverage value is 100%. The coverage is better as it approaches 100%.

Unit test frameworks can provide coverage reports. This is particularly useful because it allows identifying parts of the project that are missing automatic tests. The creation of tests able to verify these parts increases the coverage ratio, filling old gaps of untested lines of code.

Some IDEs provide features or extensions that integrate themselves with unit test frameworks and coverage tests. This allows visualizing lines that are currently covered (or uncovered) by tests, which can also make it easier to prioritize the introduction of new tests in critical parts of the project.

Manual and/or Inspection Tests

Some manual test approaches have been mentioned over Learn Programming. As part of the evolution in a programming career, it is worth improving approaches and techniques to test a program under development.

Approaches such as Test-Driven Development (TDD) can be particularly effective to help to develop higher quality software. Still, simpler techniques may be convenient during programming activities.

Test Using Print for Value Inspection

The first approach that has been commented consists in using print subroutines or commands to inspect values. This rudimentary and spartan approach has been used since the topic Development Environments: Preparing Your Computer For Software Creation.

Albeit simple, this technique is often necessary to debug real-time systems. There are buts that only happen in real-time; the insertion of a debugger can change execution times in a way the make the problem impossible to reproduce in a test environment.

Besides, there are programming languages lacking a good debugger. In other cases, the choice of a debugger may depend on operating system or of the purchase of a proprietary system (which may be expensive). In such situations, test using print statements may be the only alternative available.

In a more optimistic perspective, a benefit of the approach is that it works for any programming language and/or framework. If one knows how to write a Hello, world! in a language, she/he can use print() to debug programs written in that language.

Some programmers may defend using the approach even if there are other options. An old, albeit famous opinion (requiring a warning for potentially offensive language) comes from Linus Torvalds, the creation of Linux, about the kdb debugger to debug the kernel of the system. It is worth emphasizing the context of use for the kernel; in the same message, Torvalds comments favorably about using the gdb debugger.

The executive summary of the Python language also defends that are situations on which the quickest way to debug a program consists of using some print commands.

As usual, the opinion of author of this website is that the approaches are tools. Some techniques are more useful for a given context than to others. If one knows several techniques, she/he can apply the best one for a given situation. Therefore, tests using print are a good tool to know and be proficient.

Trace Table

The trace table (or simply trace) approach introduced in Repetition structures (or loops) is still valid as well. However, hereafter, it is worth using a debugger instead of paper to perform the test with the program running in a computer.

Debuggers

A good criterion when choosing an IDE is picking the one that provides the best debugger (or the best integration with a debugger). In fact, this was the author's recommendation in Development Environments: Preparing Your Computer For Software Creation.

The IDEs Thonny para Python, ZeroBrane Studio for Lua (documentation), and the editor of Godot Engine (documentation) provided embedded debuggers. Internet browsers for the desktop such Firefox (documentation) also provide embedded debuggers, as part of the Web Development tools.

However, perhaps ironically, there was not a suggestion to use a debugger on the last topics. In part, this is due to flowcharts and visual programming languages, which allowed visualizing how programs run when introducing fundamental concepts such as repetitions and conditions. As the next topics will introduce more complex themes and examples, it is worth officially introducing debuggers by means of their main resources.

Overview of Features in Images

The next subsections provide images with the location of the resources of debuggers that will be described in the next sections.

Firefox's Debugger for JavaScript

Firefox's debugger for JavaScript. The image has some numbers highlighting features displayed in the interface. The numbers are described in text after this image.
  1. Debugger tab;
  2. Script being debugged;
  3. Run / resume;
  4. Step over;
  5. Step in;
  6. Step out;
  7. Active breakpoint;
  8. List of breakpoints;
  9. Call stack;
  10. Watch window;
  11. Value of variable on hover.

Thonny's Debugger for Python

Thonny's debugger for Python. The image has some numbers highlighting features displayed in the interface. The numbers are described in text after this image.
  1. Start debugger;
  2. Step over;
  3. Step in;
  4. Step out;
  5. Resume;
  6. Stop;
  7. Active breakpoint;
  8. Call stack;
  9. Local and global variables;
  10. Variables in a stack frame.

ZeroBrane Studio's Debugger for Lua

ZeroBrane Studio's Debugger for Lua. The image has some numbers highlighting features displayed in the interface. The numbers are described in text after this image.
  1. Show stack window;
  2. Show watch window;
  3. Run / resume;
  4. Step in;
  5. Step over;
  6. Step out;
  7. Active breakpoint;
  8. Call stack;
  9. Watch window;
  10. Value of variable on hover.

Godot Engine's Debugger for GDScript

Godot Engine's Debugger for GDScript. The image has some numbers highlighting features displayed in the interface. The numbers are described in text after this image.
  1. Debugger tab;
  2. Debugger tab;
  3. Run;
  4. Step in;
  5. Step over;
  6. Break;
  7. Continue;
  8. Active breakpoint;
  9. Call stack;
  10. Local variables and class' attributes;
  11. Value of variable on hover.

Running Code in a Debugger

Debuggers allow running the source code of a program step by step, which means line by line. Such execution is similar to the one simulated in a trace table. The difference is that the computer computes the value of the variables and is responsible to advance to the next instruction of the program. Thus, running a program with a debugger can be similar to simulating an automated trace table.

The main features to run code through a debugger include:

  • Run: run the program normally, from the beginning to the end. If one defines breakpoints (which will be commented in the next section), the program runs until the next active breakpoint that is reached by the execution;
  • Stop: ends the program. The end can be premature, which means that one can finish the program before the last instruction or of a call to exit() or quit();
  • Pause or break: pauses the execution of the program in the last executed line;
  • Continue: continues running from the paused point. Continue is often merged with run;
  • Step over: advances to the next line of the code, without "entering" the code executed in the current line. For instance, if the code performs a function call, step over calculates the result and advances to the next line;
  • Step in: "enters" inside the code of the current line, if possible. Differently than step over, step in runs the code of a subroutine call step by step. In other words, it follows the call stack. This makes it possible to inspect the implementation of the called code;
  • Step out "exits" the running code, calculating its result. In other words, the top of the call stack is popped, returning to the code that has called it. In the last stance, this runs the program until it ends.

To better understand how the features to run code work, it can be useful to consider some simple examples.

Linear Program

To debug a program using a debugger, the first step is starting the execution using the debugger. This depends on the program that is being used. In IDEs, there are usually two options: run the project and debug the project (or run the project using a debugger). To use the debugger, one must choose the second option.

Many times, the goal is starting debugging a process that is paused or in a breakpoint. Some IDEs do this by default; to begin with a paused process, others require using a breakpoint.

To start the debugger:

  • Firefox: the JavaScript code requires an HTML page as an entry point. For instance:

    <!DOCTYPE html>
    <html lang="en-US">
    <head>
      <meta charset="utf-8">
      <title>Debugger in JavaScript</title>
      <meta name="author" content="Franco Eusébio Garcia">
      <meta name="viewport" content="width=device-width, initial-scale=1">
    </head>
    <body>
      <!-- Modify with the name of the JavaScript file. -->
      <script src="./script.js"></script>
    </body>
    </html>

    Next, one should open the development tools (for instance, using F12 in the browser) and access the option Debugger. The next step is browsing to the file in Sources. She/he must search for file://, open the directory and choose the file (for instance, script.js). Next, she/he must click to the left to the number of the first and add a breakpoint. This will add a blue symbol next to the line number. To start the debugger, she/he must update the page (F5).

    Alternatively, one can write debugger in its own line of the source code. This will be further detailed later;

  • Thonny: choose the icon of a bug next to button of play in the main interface. Alternatively, press Ctrl F5. Alternatively, use Run, then Debug current script;

  • ZeroBrane Studio: choose the icon o play in the main interface. Alternatively, press F5; Alternatively, select Project, then Start debugging;

  • Godot Engine: it is necessary to add a breakpoint. Open the file with the script in the editor. Next, click on the left side of the number of the first line with code after func _ready(). This will add an icon with a red square before the number of the line. Alternatively, keep the cursor in the line and press F9.

    To run the project, use the play icon or press F5. Alternatively, press F6 to the project in debugging mode. With a breakpoint, both options work.

// debugger

console.log("Olá, meu nome é Franco!")
console.log("Olá, meu nome é Franco!")
console.log("Olá, meu nome é Franco!")
console.log("Olá, meu nome é Franco!")
console.log("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
print("Olá, meu nome é Franco!")
extends Node

func _ready():
    print("Olá, meu nome é Franco!")
    print("Olá, meu nome é Franco!")
    print("Olá, meu nome é Franco!")
    print("Olá, meu nome é Franco!")
    print("Olá, meu nome é Franco!")

When the debugger is pausing the process' execution, one can explore the commands to control how it runs. In the interface of debugger in the IDE, she/he must search for the options to pause, continue (or run), step into, step in and step out. Then she/he should use each one to observe the results. For convenience, the shortcuts for each feature is provided below.

  • Firefox:
    • Run: one must update the page to reload it;
    • Stop: one can close the tab or change the address in the browser;
    • Pause: F8;
    • Continue: F8;
    • Step over: F10;
    • Step in: F11;
    • Step out: Shift F11;
  • Thonny:
    • Run: Ctrl F5;
    • Stop: Ctrl F2;
    • Pause: add a breakpoint to the line of interest;
    • Continue: F8;
    • Step over: F6;
    • Step in: F7;
    • Step out: the IDE does not provide a shortcut, although it provides an icon the interface;
  • ZeroBrane Studio:
    • Run: F5
    • Stop: Shift F5
    • Pause: the IDE does not provide a shortcut, although it provides an icon the interface;
    • Continue: F5;
    • Step over: Shift F10
    • Step in: Ctrl Shift F10
    • Step out: Ctrl F10;
  • Godot Engine:
    • Run: F6;
    • Stop: F8;
    • Pause: F7;
    • Continue: F12;
    • Step over: F10;
    • Step in: F11;
    • Step out: the IDE currently does not provide the option. A way to work around the issue consists of defining a breakpoint in the line after the function call and continue the execution.

To use the shortcuts, the active window must be the IDE's one.

If one uses continue, the program will end. In this case, she/he should repeat the instructions to try the other options.

Program with Alternative Flows

If one runs the program line by line with step over or step in, she/he can visualize the executed lines and the ignored lines in a program with conditional structures.

let greet = true
if (greet) {
    console.log("Olá, meu nome é Franco!")
} else {
    console.log("Bye!")
}
greet = True
if (greet):
    print("Olá, meu nome é Franco!")
else:
    print("Bye!")
local greet = true
if (greet) then
    print("Olá, meu nome é Franco!")
else
    print("Bye!")
end
extends Node

func _ready():
    var greet = true
    if (greet):
        print("Olá, meu nome é Franco!")
    else:
        print("Bye!")

If one changes greet to False, the next execution will follow the alternative path.

An advantage of IDEs and graphical environments is the possibility of hovering the mouse on a variable to inspect its value. To test the feature, one can hover the mouse on greet in the code editor of the IDE or in the browser. The value True should appear. The displayed value is modified after each assignment to the variable.

Program with Repetitions

If one runs the program line by line with step over or step in, she/he can visualize the executed lines and the ignored lines at every step of the repetition. At the end of each iteration, the code returns to the repetition structure to check if it must run a new iteration or end the loop.

let greet = true
for (let i = 0; i < 5; ++i) {
    if (greet) {
        console.log("Olá, meu nome é Franco!")
    } else {
        console.log("Bye!")
    }

    greet = !greet
}
greet = True
for i in range(5):
    if (greet):
        print("Olá, meu nome é Franco!")
    else:
        print("Bye!")

    greet = not greet
local greet = true
for i = 1, 5 do
    if (greet) then
        print("Olá, meu nome é Franco!")
    else
        print("Bye!")
    end

    greet = not greet
end
extends Node

func _ready():
    var greet = true
    for i in range(5):
        if (greet):
            print("Olá, meu nome é Franco!")
        else:
            print("Bye!")

        greet = not greet

When the loop ends, the debugger advances to the next line with code after the end of the block. If one wants to visualize the line, she/he can add something like console.log("End") or print("End") as the last line of the source code.

Program with Subroutines

With subroutines, it is interesting using step in when the current line is print_message(greet). The execution will branch to the code of the procedure print_message(_). Inside the procedure, one can use step out to return to the code that has called the subroutine, with the calculated results. She/he can also step in or step over to inspect the code of the procedure.

function print_message(greeting) {
    if (greeting) {
        console.log("Olá, meu nome é Franco!")
    } else {
        console.log("Bye!")
    }
}

let greet = true
for (let i = 0; i < 5; ++i) {
    print_message(greet)
    greet = !greet
}
def print_message(greeting):
    if (greet):
        print("Olá, meu nome é Franco!")
    else:
        print("Bye!")

greet = True
for i in range(5):
    print_message(greet)
    greet = not greet
function print_message(greeting)
    if (greeting) then
        print("Olá, meu nome é Franco!")
    else
        print("Bye!")
    end
end

local greet = true
for i = 1, 5 do
    print_message(greet)
    greet = not greet
end
extends Node

func print_message(greeting):
    if (greeting):
        print("Olá, meu nome é Franco!")
    else:
        print("Bye!")

func _ready():
    var greet = true
    for i in range(5):
        print_message(greet)
        greet = not greet

In a subroutine calls another subroutine, one can use step in again to access the code of the new call.

Breakpoints

A breakpoint marks a line of code on where the execution of the program must stop if it is reaches. This allows the developer to inspect the memory. This allows running the program automatically until the point of interest for debugging activities is reached. Thus, one can save time to start debugging, because it is not necessary to run the code line by line until the execution reaches the desired line of code.

IDEs typically allow enabling and disabling a breakpoint by clicking next to the line, usually on the left side of the embedded text editor. After clicking the position, an icon such as a circle or square will appear to represent the break. If one clicks again on the icon, she/he will remove or disable the breakpoint.

It is also possible to activate breakpoints using keyboard shortcuts. To do this, one should leave the keyboard cursor in the desired line and press:

  • Firefox: Ctrl B. It is also possible to add a breakpoint to the code writing debugger (documentation) in a line of code;
  • Thonny: the author does not know the shortcut. An alternative is inserting breakpoint() (documentation) to a line of code, which starts Python's embedded debugger (documentation). However, this does not trigger the IDE's graphical debugger;
  • ZeroBrane Studio: Ctrl F9;
  • Godot Engine: F9.

Conditional Breakpoints

An advanced feature available in some debuggers is called conditional breakpoint. It can be used in (documentation).

A conditional breakpoint allows defining a conditional to activate a breakpoint. Conditional breakpoints can make it even faster to debug a program, because they allow to inspect the code at the moment that the process has the desired data. For instance, one can activate a breakpoint only if a variable has a specific value (such as name == "Franco") or belongs to an interval of interest (such as (number > 0) and (number < 10)).

To simulate a conditional breakpoint in debuggers that no dot provide one, an alternative is defining a condition in the source code and a common breakpoint inside it. For instance:

local name = "..."
-- ...

if (name == "Franco") then
    -- Add the breakpoint here.
    local add_breakpoint_here = 0
    print("Time to inspect the code!")
end

After fixing the problem, one must remember to remove the code that activates the breakpoint.

Watches to Observe Variables

Debuggers often show values for local and global variables in panels at their interfaces. In some debuggers, it is possible to click or browse in the displayed values to inspect complex variables, such as records, objects (from classes), arrays and dictionaries.

Furthermore, when one desired to monitor the value of a variable over the execution, she/he can watch it to avoid requesting the value (or hovering the mouse on the variable) every step of the program.

The feature to watch variables exist in some debuggers. However, the way to activate it regularly varies. Normally it is necessary to search for a panel called Watch or similar, and add a variable.

  • Firefox: search for Watch expressions and use the plus signal (+) to write the name of the variable;
  • Thonny: currently does not offer the feature;
  • ZeroBrane Studio: first it is necessary to activate the window, using the shortcut Ctrl Shift W or clicking the icon of the main interface (a window with glasses). Next, one can right-click in the window that appears and choose the option Add watch (shortcut: Insert). Alternatively, she/he can right-click the name of a variable in the source code and pick the option Add Watch Expression. It is also possible to activate the option in Shown, then select Watch;
  • Godot Engine: currently does not offer the feature.

Advanced implementations are not restricted to variables. For instance, they can allow analyzing expressions, get results from function calls, or access memory values from references (or even custom raw addresses).

Call Stack

Another traditional feature of debuggers is a panel to show the call stack of the program. The stack increases at every subroutine call, and decreases at every subroutine return.

  • Firefox: available in the Debugger, panel, in the section Call Stack;
  • Thonny: it must be activated in View, option Stack;
  • ZeroBrane Studio: the window must be activated using Ctrl Shift S or an icon in the main interface (three windows in sequence with an arrow between the second and third ones). It can also be activated in View, then choosing the option Stack window;
  • Godot Engine: available in the Debugger panel, with the name Stack Frames.

Normally it is possible to double-click an option of the stack to switch the context. This allows, among other options, to view values of variables on other levels of the stack.

It is interesting to observe the call stack during a call to a recursive function.

// debugger

function revert_string(message, index) {
    if (index < 0) {
        return ""
    }

    let result = message[index] + revert_string(message, index - 1)

    return result
}

function print_backwards(message) {
    let result = revert_string(message, message.length - 1)
    console.log(result)
}

print_backwards("!ocnarF é emon uem ,álO")
def revert_string(message, index):
    if (index < 0):
        return ""

    result = message[index] + revert_string(message, index - 1)

    return result

def print_backwards(message):
    result = revert_string(message, len(message) - 1)
    print(result)

print_backwards("!ocnarF é emon uem ,álO")
function revert_string(message, index)
    if (index < 1) then
        return ""
    end

    local result = string.sub(message, index, index) .. revert_string(message, index - 1)

    return result
end

function print_backwards(message)
    local result = revert_string(message, #message)
    print(result)
end

print_backwards("!ocnarF é emon uem ,álO")
extends Node

func revert_string(message, index):
    if (index < 0):
        return ""

    var result = message[index] + revert_string(message, index - 1)

    return result

func print_backwards(message):
    var result = revert_string(message, len(message) - 1)
    print(result)

func _ready():
    print_backwards("!ocnarF é emon uem ,álO")

To watch the stack growing during recursive calls, one must use step in each every new call. The stack will keep growing until index becomes negative (or zero, in Lua). At this moment, the functions will start returning, and the stack will progressively decrease, until it ends reversing message.

If the base case of recursion is omitted, the stack will keep growing until it consumes the entire memory that it can allocate -- at this moment, a stack overflow happens.

The version in Lua is particularly interesting because it does not handle UFT-8 values. Consequently, the á will be split in two values, that are the bytes that would form the accented character. The reason and the correct way to iterate the values have been explained in Arrays, strings, collections and data structures. To keep the example similar to the other languages, the implementation iterates on the bytes of the string (instead of a UTF-8 code point).

Strategies and Techniques for Debugging and Testing

As one acquire programming experience, she/he will develop strategies and techniques to test and debug programs. For tests, they serve both when creating tests (such as unit tests), and to test the code after writing it. Techniques for debugging, on the other hand, help to identify a bug and fix it.

The next section describe useful strategies and techniques for testing and debugging, as well as some common technical terms. Hereafter, the expression quality assurance, more popular by its acronym QA, should be part of your programming vocabulary and practices.

Intervals of Values

A way to guarantee that a solution is correct consists of performing exhaustive tests, which evaluates every possible input value for a system (or subroutine) and check whether the provided results are correct.

Evidently, this may not be viable. A useful heuristic consists in identifying intervals of values on which the result (or the way to calculate it) may vary. For instance, in a code that process an array with 10 values, it is worth testing:

  • A value at an intermediate index, such as 7;
  • The value in the first index (0 or 1, depending on the language);
  • The value in the last index (9 or 10, depending on the language);
  • The index before the first value (-1 or 0), to guarantee that it is not processed. In languages on which the index -1 access the last value of the array, the text must be adjusted accordingly;
  • The index after the last value (10 or 11), to guarantee that it is not processed.

The reason for the choices is that a typical implementation of an algorithm that processes arrays use a repetition structure. Errors by one in indices are common in programming, to the point of having a special name: off-by-one error. The creation of test cases that the previous index provide a heuristic that every value has been suitably processing in a loop.

It is even better to consider an array with variable size and define an exhaustive test based in an array with a few elements. For a smaller example, the array could have 3 positions. This would make it easier to test all possible indices and expected results. Assuming that the loop uses the size of the array, the implementation should also work correctly for larger sizes.

The choice of minimum and maximum values in a range is a useful technique for software testing. In particular, these extreme values have a special name: edge case.

Special Cases

Several algorithms have values that are processed equally or similarly. This processing is generic and valid for classes of values, generating the happy path for the execution.

For instance, in the case of the division, the operation works correctly for every real number, except zero. The division by zero has two problems:

  1. Zero denominator for non-zero numerators. In the case of real numbers, this can result in infinity (as an approximation) in floating-point arithmetic. In the case of integer numbers, it must be avoided;
  2. Zero by zero division, which is indeterminate in Mathematics.

Special cases require special handling and specific test cases. Therefore, it is important anticipating them and handling them correctly in the implementation.

In particular, the author prefers handling special cases as soon as possible, using the technique of early return, introduced in Subroutines (Functions and Procedures). This choice allows eliminating special cases to focus on the happy path.

Pathological Cases (Corner Cases)

An special cases that requires specific condition to happen is called a pathological case or corner case. Differently from edge cases, corner cases can be difficult to anticipate, because they can happen in situations that are so particular that they are unknown when designing a solution. For instance, they can depend on a specific hardware configuration, of a rare sequence of interactions that can generate a propitious computation state that is required to trigger the error, or even of conditions that are external to the program.

Unfortunately, it can be heard to reproduce a corner cases; therefore, it can be equally hard to debug and fix it. Regardless, it is worth defining a special test case to consider it, as soon as it has been identified (or noticed by someone). In the worst scenario, it should be a known issue of the project.

Isolating a Region

When one debugs a code with bugs, one of the first steps it trying to identify the region (code area) on which the problem(s) happens. The goal is to keep reducing the region, until she/he finds the root of the issue.

To do this, it is worth considering scopes and subroutines. Local scopes and subroutines reduce the region because they restrict the access and modification of values in the memory, and they also delimit instructions. When one works with local scope and subroutines, she/he reduces the locations that she/he is searching for the issue. In general, the problem:

  1. Will be located in the values passed in the call to the subroutine. In other words, a wrong value has been passed as a parameter. This can happen due to a typing error, the use of an incorrect variable, or the use of a variable with an incorrect value (previously calculated);
  2. Will be in the value returned by the subroutine. This means that the subroutine has received correct values. Therefore, the problem will probably be in the implementation of the subroutine.

The analysis of the call stack can be particularly useful, for it reveals the performed calls to the subroutines, providing a history of recent and potentially related operations. When one identifies points of interest, she/he can mark them with breakpoints to automatically run the project with a debugger until they are reached.

On the other hand, the global scope can increase the space to the entire program. In potential, every part of the project that manipulates the variable can be the root of the issue. It is even worse when the project has threads or parallel code, because the state to trigger the bug may require specific timings or interactions to happen.

Therefore, this is an extra motive to prefer local scope and modularization.

Minimal Working Example

A particularly useful way to resolve complex bugs consists in creating a minimal example that implements and demonstrates the problem -- and nothing else. This is called a Minimal, Reproducible Example or a Minimal Working Example.

The principle is reducing the problem to the smaller and simpler implementation that can show it and produce it deterministically. This is useful because, among other reasons:

  1. Allows focusing on the problem at hand, without interference of other parts of the implementation that are not of interest;
  2. Reduces the required time to modify the implementation, run the project, and check the result of the modification;
  3. Provides a smaller example that can be shown to other people. This is particularly useful, for instance, if one asks help to her/his friends, peers, or on Internet forums, such as Stack Overflow. Actually, the Stack Overflow website provide guidelines to create minimal working examples.

When one requests help of other people, the creation of a minimal working example is useful to the other person, because it reduces the time necessary to understand the problem. It is also useful for the person who requested the help, because she/he will not need to share the entire project. This, among others, can avoid sharing private code or data, reduces the size of the shared code, and minimizes external dependencies.

New Items for Your Inventory

Tools:

  • Debugger;

Skills:

  • Debugging projects;
  • Testing software;
  • Handling errors.

Concepts:

  • Patch;
  • Defensive programming;
  • Error handling;
  • Error code;
  • Exceptions;
  • Overhead;
  • Traceback;
  • Logging;
  • Code coverage;
  • Debugging;
  • Debugger;
  • Step over;
  • Step in;
  • Step out;
  • Breakpoint;
  • Conditional Breakpoint;
  • Watch;
  • Call stack;
  • Exhaustive testing;
  • Interval of values;
  • Happy path;
  • Edge cases;
  • Corner cases;
  • Minimal working example.

Programming resources:

  • Exception;
  • Error handling;
  • Variadic functions.

Practice

  1. Create unit tests for exercises or projects that you created in previous topics;
  2. Use a debugger to analyze a project created by another person. To do this, you can search for open source projects in the programming language of your preference.
  3. Think about strategies and techniques to test and debug software that you have used over your programming activities. How can you improve them? What are the situations on which you can apply them? How can you introduce debuggers and other techniques presented in this topic to improve your test and debugging processes?

Next Steps

Tests and debugging are activities that are part of every software process. The practices can be formal or informal; however, they are always present. After all, one of the first steps after writing a Hello, world! was running the project to verify if the message would, in fact, appear in the screen.

Software development is complex. It is very common that a project does not compile or work correctly immediately after it has been written. In fact, experienced people tend to be suspicious of their own solution that has worked correctly in the first use. It is possible that there is an edge case or corner case that has not been anticipated.

Anyway, there is a topic that has not been explored yet for basic concepts: memory and pointers. The intention of the author is exploring it in the future, possibly in a programming series using the C programming language or the C++ programming language.

As anticipated in Bitwise Operations and in some previous topics, the current plan is start a new series focused on simulations. It is time to try your programming abilities in practice, in projects that are more dynamic and interactive.

  1. Introduction;
  2. Entry point and program structure;
  3. Output (for console or terminal);
  4. Data types;
  5. Variables and constants;
  6. Input (for console or terminal);
  7. Arithmetic and basic Mathematics;
  8. Relational operations and comparisons;
  9. Logic operations and Boolean Algebra;
  10. Conditional (or selection) structures;
  11. Subroutines: functions and procedures;
  12. Repetition structures (or loops);
  13. Arrays, collections and data structures;
  14. Records (structs);
  15. Files and serialization (marshalling);
  16. Libraries;
  17. Command line input;
  18. Bitwise operations;
  19. Tests and debugging.
  • Informatics
  • Programming
  • Beginner
  • Computational Thinking
  • Learn Programming
  • Python
  • Lua
  • Javascript
  • Godot
  • Gdscript