Skip to main content

A Python testing tool that combines fuzzing and traditional unit tests.

Project description

PyVeritas

Robust, user-friendly unit testing and fuzzing support for your Python application. Designed to be simple (easy to code your tests) and accessible (all of your testing in the one place).

🚀 Overview

PyVeritas is your easy-to-use software testing framework. PyVeritas combines unit testing, and the randomness of fuzzing to help you ensure the reliability and quality of your Python code.

🌟 Features

Fuzzing: Create meaningful fuzzing inputs tailored to your specific code logic.

Unit Testing: Write and run lean and relevant tests for your existing Python code.

🛠️ Installation

PyVeritas is easy to install via pip:

pip install pyveritas

✨ Quick Start

Example, testing a calculator function. Let's say you had a Python file called my_code.py where you had implemented a calculate_discount() function.

If you add the run_unit_tests() and run_fuzz_tests(), as shown below, you can ensure the robustness of your code:

import argparse
from pyveritas.unit import VeritasUnitTester
from pyveritas.fuzz import VeritasFuzzer

def convert_celsius_to_fahrenheit(celsius):
    """Convert temperature from Celsius to Fahrenheit."""
    return (celsius * 9/5) + 32

def calculate_distance(lat1, lon1, lat2, lon2):
    """Calculate the distance between two points on earth in kilometers."""
    from math import radians, sin, cos, sqrt, atan2
    R = 6371  # Earth radius in kilometers

    lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
    dlat = lat2 - lat1
    dlon = lon2 - lon1

    a = sin(dlat / 2)**2 + cos(lat1) * cos(lat2) * sin(dlon / 2)**2
    c = 2 * atan2(sqrt(a), sqrt(1 - a))

    return R * c

def validate_ip_address(ip):
    """Validate if the given string is a valid IP address."""
    import re
    pattern = re.compile(r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$')
    return bool(pattern.match(ip))

def validate_email(email):
    """Validate if the given string is a valid email address."""
    import re
    pattern = re.compile(r"(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)")
    return bool(pattern.match(email))

def original_script_logic():
    """Demonstrates the functionality of each function with example parameters."""
    print(f"Convert 25°C to Fahrenheit: {convert_celsius_to_fahrenheit(25)}°F")
    print(f"Distance between Berlin and London: {calculate_distance(52.5200, 13.4050, 51.5074, -0.1278):.2f} km")
    print(f"IP Address '192.168.0.1' valid: {validate_ip_address('192.168.0.1')}")
    print(f"IP Address '256.1.2.3' valid: {validate_ip_address('256.1.2.3')}")
    print(f"Email 'test@example.com' valid: {validate_email('test@example.com')}")
    print(f"Email 'invalid.email@' valid: {validate_email('invalid.email@')}")

def run_unit_tests():
    """Runs unit tests for the IoT functions."""
    unit_tester = VeritasUnitTester("IoT Unit Tests")
    
    # Unit Tests
    unit_tester.add(
        "Convert 0°C to 32°F",
        convert_celsius_to_fahrenheit,
        [{"input": [{"name": "celsius", "value": 0}], "output": [{"name": "result", "value": 32, "type": "float"}]}]
    )

    unit_tester.add(
        "Calculate distance between two points",
        calculate_distance,
        [
            {
                "input": [
                    {"name": "lat1", "value": 52.5200, "type": "float"},
                    {"name": "lon1", "value": 13.4050, "type": "float"},
                    {"name": "lat2", "value": 51.5074, "type": "float"},
                    {"name": "lon2", "value": -0.1278, "type": "float"}
                ],
                "output": [{"name": "distance", "value": 925.8, "type": "float"}]  # Approximate distance in km
            }
        ]
    )

    unit_tester.add(
        "Validate IP Address",
        validate_ip_address,
        [
            {"input": [{"name": "ip", "value": "192.168.0.1"}], "output": [{"name": "is_valid", "value": True}]},
            {"input": [{"name": "ip", "value": "256.1.2.3"}], "output": [{"name": "is_valid", "value": False}]}
        ]
    )

    unit_tester.add(
        "Validate Email Address",
        validate_email,
        [
            {"input": [{"name": "email", "value": "test@example.com"}], "output": [{"name": "is_valid", "value": True}]},
            {"input": [{"name": "email", "value": "invalid.email@"}], "output": [{"name": "is_valid", "value": False}]}
        ]
    )

    unit_tester.run()
    unit_tester.summary()

def run_fuzz_tests():
    """Runs fuzz tests for the IoT functions."""
    fuzz_tester = VeritasFuzzer("IoT Fuzz Tests")

    # Fuzz Tests
    fuzz_tester.add(
        "Fuzz temperature conversion",
        convert_celsius_to_fahrenheit,
        [
            {
                "input": [
                    {"name": "celsius", "type": "float", "range": {"min": -100, "max": 100}}
                ],
                "output": [],
                "iterations": 100
            }
        ]
    )

    fuzz_tester.add(
        "Fuzz IP validation",
        validate_ip_address,
        [
            {
                "input": [
                    {"name": "ip", "type": "str", "regular_expression": r"\b(?:\d{1,3}\.){3}\d{1,3}\b"}
                ],
                "output": [],
                "iterations": 1000
            }
        ]
    )

    fuzz_tester.add(
        "Fuzz Email validation",
        validate_email,
        [
            {
                "input": [
                    {"name": "email", "type": "str", "regular_expression": r"[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+"}
                ],
                "output": [],
                "iterations": 1000
            }
        ]
    )

    fuzz_tester.run()
    fuzz_tester.summary()

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Run IoT functions or perform tests")
    parser.add_argument("--unit", action="store_true", help="Run unit and fuzz tests")
    parser.add_argument("--fuzz", action="store_true", help="Run unit and fuzz tests")    
    args = parser.parse_args()

    if args.unit:
        run_unit_tests()
    elif args.fuzz:
        run_fuzz_tests()
    else:
        original_script_logic()

When calling the test and fuzz as arguments, your code (example as shown above) will return something similar to the following:

python3 tests/my_code.py --unit

Screenshot 2025-02-03 at 15 34 20

python3 tests/my_code.py --fuzz

Screenshot 2025-02-03 at 15 33 38

The code of the file will also run alone (meaning that your Python file can maintain functionality in your environment and only ever execute the tests when you specify the --unit or --fuzz arguments):

python3 tests/my_code.py --fuzz

Screenshot 2025-02-03 at 15 33 56

PyVeritas aims to simplify advanced testing scenarios without requiring users to write extensive boilerplate code or understand complex concepts.

JSON Test Structure

{
    "enabled": 1,
    "description": "Test for generating sales report summary",
    "input": [
        {
            "name": "sales_data",
            "value": "[{\"product\": \"Gadget\", \"units\": 100}, {\"product\": \"Widget\", \"units\": 50}]",
            "type": "str",
            "regular_expression": "[{\"product\": \"[A-Za-z]+\", \"units\": \"\\d+\"}(,{\"product\": \"[A-Za-z]+\", \"units\": \"\\d+\"})*]"
        },
        {
            "name": "report_type",
            "value": "Summary",
            "type": "str",
            "regular_expression": "^(Summary|Detailed)$"
        },
        {
            "name": "customer_id",
            "value": 12345,
            "type": "int",
            "regular_expression": "[A-Za-z0-9]{5,10}"
        },
        {
            "name": "price",
            "value": 100,
            "type": "float",
            "range": {
                "min": 50,
                "max": 100
            }
        }
    ],
    "output": [
        {
            "name": "total_units",
            "value": 150,
            "type": "int"
        },
        {
            "name": "company_id",
            "value": 12345,
            "type": "int",
            "regular_expression": "[A-Za-z0-9]{5,10}"
        },
        {
            "name": "price",
            "value": 100,
            "type": "float",
            "range": {
                "min": 50,
                "max": 100
            }
        }
    ],
    "exception": "ValueError",
    "exception_message": "Invalid sales data format or report type",
    "iterations": 1000
}

Test Enablement

Whether the test is enabled or not.

Description

A brief explanation of what the test is for.

Input Array

Name

Name of the parameter

Example value

Value of the parameter

Type

Type of the parameter

Regular Expression

Regular expression used to generate value when fuzz testing

Range

A min and max value used to generate value when fuzz testing

Output Array

An array specifying expected outputs

Name

Name of the output

Example value

Value of the output

Type

Type of the parameter

Exception

The expected exception to be returned. This is for testing that a function will actually throw a specific exception under certain circumstances.

Exception Message

The message returned when an exception is returned.

Iterations

The amount of fuzz tests to generate


Some of the above fields are optional and some take precedence. The following paragraph explains optional/mandatory and precedence behaviour.

Input:
The input array can be empty or contain multiple items. Each item must have:
A name and a type.
One of the following for value specification:
Explicit value: If present, this value is used directly for unit testing; no fuzzing occurs.
Regular expression (regular_expression): Used for fuzzing; PyVeritas generates values according to this expression.
Range (range): Also for fuzzing; values are generated within the specified min and max.
Precedence and Behavior:
value has the highest precedence, used for unit testing.
In the absence of value, regular_expression or range is used for fuzzing.
If both regular_expression and range are specified, values are generated using the regular expression but then filtered by the range. If the range cannot filter the generated value or if min is not less than max, testing stops with an error.
Error Handling: If neither value, regular_expression, nor a properly set range is present for an input item, testing stops with an error.

Output:
If specified, exception and exception_message are checked against the actual exceptions thrown during testing.

Iterations:
iterations only applies to fuzzing tests. 
Fuzzing occurs when any input uses regular_expression or range.
Tests with only explicit values run once.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pyveritas-0.1.5.tar.gz (7.7 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pyveritas-0.1.5-py2.py3-none-any.whl (12.6 kB view details)

Uploaded Python 2Python 3

File details

Details for the file pyveritas-0.1.5.tar.gz.

File metadata

  • Download URL: pyveritas-0.1.5.tar.gz
  • Upload date:
  • Size: 7.7 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.3

File hashes

Hashes for pyveritas-0.1.5.tar.gz
Algorithm Hash digest
SHA256 d25f6ab6ab14da184e143bb07b9ad63efdfdf26ac56b09f8732c1d074e0abff9
MD5 1e6340faefec262632c1c866c1c5ebce
BLAKE2b-256 1163c974d3e4689b81b24155827668f13734b0ffb8b02946fc9519a97f3078fb

See more details on using hashes here.

File details

Details for the file pyveritas-0.1.5-py2.py3-none-any.whl.

File metadata

  • Download URL: pyveritas-0.1.5-py2.py3-none-any.whl
  • Upload date:
  • Size: 12.6 kB
  • Tags: Python 2, Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: python-requests/2.32.3

File hashes

Hashes for pyveritas-0.1.5-py2.py3-none-any.whl
Algorithm Hash digest
SHA256 d406c955b5d3dfac5b20296886021ea05463cbb6c59cd683888347127e1cec89
MD5 b56c5668528123e0e503a1a32af53011
BLAKE2b-256 a2a113e7753de7cdc64ef51e2c578d093cb331fdb75999433f2d4eca87a6eb73

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page