Python Testing & Exceptions

"Software testing and exception handling are essential components of quality software development. Testing identifies potential defects and ensures the software meets requirements, while exception handling prevents unexpected crashes and provides a way to recover from errors. By combining effective testing practices with robust exception handling, you can create reliable and user-friendly applications."- Gemini 2024

Software Testing

Software testing is a critical component of the software development lifecycle, ensuring that applications function as intended, meet user requirements, and maintain reliability under various conditions. Thorough testing helps identify bugs, security vulnerabilities, and performance issues before they impact end-users, ultimately saving time, resources, and reputation. By implementing a comprehensive testing strategy, developers can improve code quality, enhance user experience, and reduce the risk of costly post-release fixes. Moreover, testing facilitates smoother updates and maintenance, as well as compliance with industry standards and regulations.

Test Early, Test Often
Type of Testing Description
Unit Testing Verifies individual components or functions of the code
Integration Testing Checks how different modules or services work together
Functional Testing Ensures the software meets specified functional requirements
Performance Testing Evaluates system responsiveness and stability under various load conditions
Security Testing Identifies vulnerabilities and potential security breaches
Usability Testing Assesses the user-friendliness and intuitiveness of the interface
Regression Testing Confirms that recent code changes haven't adversely affected existing functionality
Acceptance Testing Determines if the software meets business requirements and is ready for delivery
Stress Testing Examines system behavior under extreme conditions
Compatibility Testing Checks software performance across different environments, devices, and browsers

Testing Example - Basic Function
Main | Test | Run

This code demonstrates:

  1. A basic `clean_words` function that cleans text and separates words.
  2. Pytest unit tests for the function.
    • Basic functionality testing
    • Edge case testing (empty input)
    • Specific case testing (special characters, numbers, case)

To run these tests, you would need to have pytest installed (pip install pytest) and then you can run pytest.

PyTest Tips

When naming your test files for pytest, follow the convention of using the prefix or suffix test_ in your filename. For example:

  • Use test_*.py as a prefix: test_module.py
  • Or use *_test.py as a suffix: module_test.py

This naming convention allows pytest to automatically discover and collect your test files when you run the pytest command without explicitly specifying file names.

Additionally, ensure that your test function names also start with test_ for pytest to recognize them as test cases.

By following these naming conventions, you'll make it easier for pytest to find and run your tests, and you'll also improve the organization and readability of your test suite.

import re
from typing import List

def clean_words(text: str) -> List[str]:
    """
    Process the input text by removing special characters,
    converting to lowercase, and splitting into words.
    """
    cleaned = re.sub(r'[^a-zA-Z\s]', '', text.lower())
    return cleaned.split()
import pytest
from cleaner import clean_words

def test_cleaner():
    text_in = 'LOL! This is adorable #love 🤩'
    text_out = ['lol', 'this', 'is', 'adorable', 'love']
    assert clean_words(text_in) == text_out

def test_cleaner_empty():
    assert clean_words('') == []

def test_cleaner_numbers():
    assert clean_words('call 734-567-8901') == ['call']

def test_cleaner_lowercase():
    assert clean_words('YES') == ['yes']

Testing Example - NLP Function
This code demonstrates:
  1. A basic `preprocess` function that cleans and tokenizes text.
  2. Pytest unit tests for the function.
    • Basic functionality testing
    • Edge case testing (empty input)
    • Specific case testing (special characters, numbers, stop word removal)
from typing import List

import nltk
from nltk.corpus import stopwords
from nltk.tokenize import word_tokenize

nltk.download('punkt')
nltk.download('stopwords')

def preprocess(text: str) -> List[str]:
    """
    Process the input text using NLP techniques:
    tokenization, lowercasing, and stop word removal.
    """
    tokens = [word.lower() for word in word_tokenize(text)]
    omit = set(stopwords.words('english'))
    return [x for x in tokens if x.isalpha() and x not in omit]
from preprocess import preprocess

def test_preprocess():
    input_text = 'The frog is on a log.'
    expected_output = ['frog', 'log']
    assert preprocess(input_text) == expected_output

def test_preprocess_empty():
    assert preprocess('') == []

def test_preprocess_stopwords():
    input_text = 'This is a test of the function.'
    expected_output = ['test', 'function']
    assert preprocess(input_text) == expected_output

def test_preprocess_alpha():
    assert preprocess('Hello 7') == ['hello']

Exception Handing

The Python language contains built-in exceptions, in addition to allowing developers to create custom exceptions.

Testing with Exceptions
import math

class NegativeSquareRootError(ValueError):
    """
        Custom exception for attempting to calculate square root
        of a negative number.
    """
    def __init__(self, val):
        self.val = val
        self.msg = f'Cannot get square root of negative: {val}'
        super().__init__(self.msg)

class Calculator:
    def add(self, a, b):
        return a + b

    def subtract(self, a, b):
        return a - b

    def multiply(self, a, b):
        return a * b

    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

    def square_root(self, a):
        if a < 0:
            raise NegativeSquareRootError(a)
        return math.sqrt(a)
import pytest

from calculator import Calculator, NegativeSquareRootError

# Fixture to create a Calculator instance for each test
@pytest.fixture
def calc():
    return Calculator()

def test_add(calc):
    assert calc.add(2, 3) == 5

def test_subtract(calc):
    assert calc.subtract(5, 3) == 2

def test_multiply(calc):
    assert calc.multiply(2, 3) == 6

def test_divide(calc):
    assert calc.divide(6, 3) == 2
    with pytest.raises(ValueError):
        calc.divide(1, 0)

def test_square_root(calc):
    assert calc.square_root(4) == 2
    with pytest.raises(NegativeSquareRootError) as excinfo:
        calc.square_root(-4)

Test Coverage: Striving for Completeness in a Complex World
Content Information

This section was generated by Claude AI, an artificial intelligence language model. While efforts have been made to ensure accuracy and relevance, please review and verify any critical information. (Disclaimer: This disclaimer was also written by Claude AI.)

Test coverage is a crucial metric in software development, measuring the extent to which a codebase is exercised by a test suite. Ideally, we aim for 100% code coverage, meaning every line of code, every decision branch, and every edge case is tested. This goal ensures that all parts of the system have been examined and verified to work as intended.

However, achieving 100% test coverage is often impractical or even impossible in real-world scenarios due to several factors:

  1. Combinatorial Explosion: As the complexity of software increases, the number of possible input combinations and execution paths can grow exponentially. Testing every possible combination becomes infeasible due to time and resource constraints.

  2. Environmental Factors: Some code paths may depend on external systems or specific environmental conditions that are difficult to replicate in a test environment.

  3. Edge Cases: Certain edge cases or error conditions may be extremely rare or difficult to trigger in a controlled test setting.

  4. Continuous Evolution: Software is often in a state of constant change, making it challenging to maintain complete coverage as new features are added or existing ones are modified.

  5. Cost-Benefit Ratio: The effort required to achieve the last few percentage points of coverage often outweighs the benefits, especially for rarely executed code paths.

Given these challenges, a more practical approach is to set a realistic coverage goal that balances thoroughness with efficiency. Many organizations and projects aim for a coverage threshold between 80% and 90%. This range typically ensures that:

While striving for this target, it's crucial to remember that coverage is just one aspect of a comprehensive testing strategy. Other important factors include:

Ultimately, the goal should be to create a robust, maintainable test suite that provides confidence in the software's reliability and functionality, rather than blindly pursuing a coverage percentage. Regular code reviews, risk analysis, and prioritization of testing efforts based on the criticality of different components can help ensure that the most important aspects of the system are thoroughly tested, even if 100% coverage remains elusive.


Coverage with pytest - calculator example

To run these tests, you would need to have pytest-cov installed (pip install pytest-cov).