Running Property-Based Testing for Smart Contracts

Property-based testing is a fancy way of saying “testing with examples.” Instead of writing tests that specifically check if your code does one thing or another (like “does this function add two numbers together?”), property-based testing involves creating test cases based on properties that should hold true for any input. For example, you might write a test case like:

# This function is used to test the property of addition, where the sum of a list of numbers should be equal to the sum of all numbers from 1 to the length of the list.
def test_addition(numbers):
    # The assert statement checks if the condition is true, if not, it will raise an AssertionError.
    assert sum(numbers) == len(numbers) * (len(numbers) + 1) // 2 # The sum of numbers should be equal to the sum of all numbers from 1 to the length of the list, divided by 2.
    # The "//" operator performs integer division, which returns the quotient without the remainder.
    # This ensures that the sum of numbers is an integer, as the length of the list and the sum of all numbers from 1 to the length of the list will always be even.
    # This also accounts for the fact that the list may contain duplicate numbers, as the sum of all numbers from 1 to the length of the list will still be the same.
    # This test case will pass for any input that follows the property of addition.

This tests the property that “the sum of any list of numbers is equal to half the product of its length and one plus its length.” Pretty cool, right? And it’s way more flexible than writing a bunch of specific test cases for every possible input.

But what about smart contracts? How can we apply this to our beloved blockchain technology? Well, let’s say you have a contract that calculates the square root of a number. You could write tests like:

# This function is used to test the functionality of a SquareRootContract by comparing its output to the square root of a given number.
def test_square_root(numbers):
    # The "numbers" parameter is a list of numbers that will be used as inputs for the SquareRootContract.
    for num in numbers:
        # The "num" variable represents each number in the "numbers" list.
        # The "assert" statement checks if the output of the SquareRootContract is equal to the square root of the given number.
        assert SquareRootContract().calculateSquareRoot(num) == math.sqrt(num)
        # The "SquareRootContract()" creates an instance of the SquareRootContract class.
        # The ".calculateSquareRoot(num)" method calculates the square root of the given number.
        # The "math.sqrt(num)" function calculates the square root of the given number using the math module.

This checks that the contract’s output is equal to the square root of each input number, but it only works for a limited set of inputs (i.e., any integer between 0 and some maximum value). What if we want to test more complex properties? Like:

– The function should always return a positive result when given a non-negative input
– If the input is zero or negative, the output should be zero
– If the input is very large (e.g., 10^20), the output should still be within reasonable bounds (i.e., not overflowing any memory limits)

That’s where property-based testing comes in! Instead of writing specific test cases for each scenario, we can create a set of properties that should hold true for all inputs and let our tool do the heavy lifting. For example:

# This function tests the square root calculation for a given set of numbers
def test_square_root(numbers):
    # Define some properties to check
    # This function checks if a number is non-negative
    def is_nonnegative(x): return x >= 0
    # This function checks if a number is zero or negative
    def is_zero_or_negative(x): return x <= 0
    # This function checks if the result is within reasonable bounds
    def is_within_bounds(x, y): return abs(y) < 1e20
    
    # Create a test case that checks all three properties at once
    for num in numbers:
        # If the number is not non-negative, skip it
        if not is_nonnegative(num): continue
        # Calculate the square root using the SquareRootContract class
        result = SquareRootContract().calculateSquareRoot(num)
        # Assert that the result is either zero or negative, or within reasonable bounds
        assert is_zero_or_negative(result) or is_within_bounds(num, result)

This tests the properties that “the function should always return a positive result when given a non-negative input,” “if the input is zero or negative, the output should be zero,” and “if the input is very large (e.g., 10^20), the output should still be within reasonable bounds.” And it does so for any possible input!

It’s a powerful tool that can help catch bugs and ensure your code is working as expected, without having to write tons of specific test cases. Give it a try !

SICORPS