DEV Community

Cover image for Python Testing Techniques Every Developer Should Master in 2024: TDD, Mocking & CI
Nithin Bharadwaj
Nithin Bharadwaj

Posted on

Python Testing Techniques Every Developer Should Master in 2024: TDD, Mocking & CI

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

Testing is like having a safety net for your code. It helps make sure that everything works the way it should, even when things change. I think of it as a way to catch mistakes before they cause bigger problems. When I write code, I always include tests to verify that each part does its job correctly. This saves me time later because I can spot issues quickly.

Let me start with unit testing. This is where you test small pieces of your code, one at a time. It is like checking each ingredient before cooking a meal. In Python, you can use the unittest framework to do this. It lets you create test cases and check if the results match what you expect.

Here is a simple example. Suppose I have a function that adds two numbers. I want to test it to make sure it works for different inputs.

import unittest

def add(a, b):
    return a + b

class TestAddition(unittest.TestCase):
    def test_add_positive_numbers(self):
        result = add(2, 3)
        self.assertEqual(result, 5)

    def test_add_negative_numbers(self):
        result = add(-1, -1)
        self.assertEqual(result, -2)

    def test_add_zero(self):
        result = add(0, 5)
        self.assertEqual(result, 5)

if __name__ == '__main__':
    unittest.main()
Enter fullscreen mode Exit fullscreen mode

When I run this, it checks each test. If any fail, I know something is wrong. I find this very helpful because it catches errors early. For instance, once I had a function that was supposed to calculate discounts, but it gave wrong results for negative values. Unit testing helped me fix it before it affected users.

Another technique is test-driven development, or TDD. This means writing tests before you write the actual code. It might sound backward, but it helps you think about what the code should do. I start by writing a test that fails, then write code to make it pass. This way, I only write code that is needed.

Let me show you how I use TDD with pytest, which is a popular testing tool in Python. It has a simple syntax that I like.

# First, write a test for a function that checks if a number is even
def test_is_even():
    assert is_even(4) == True
    assert is_even(5) == False

# Initially, this test fails because is_even doesn't exist
# Now, implement the function
def is_even(number):
    return number % 2 == 0

# Run the test again, and it should pass
Enter fullscreen mode Exit fullscreen mode

I remember using TDD for a project where I was building a calculator. I wrote tests for addition, subtraction, and other operations first. This made me confident that each new feature worked correctly from the start.

Mocking is another important technique. Sometimes, your code depends on other parts, like databases or web services, that are not available during testing. Mocking lets you create fake versions of these dependencies. This way, you can test your code in isolation.

In Python, the unittest.mock module is very useful. I often use it to simulate API calls or database queries.

from unittest.mock import Mock, patch

def get_user_email(user_id):
    # This function might call a database
    # For testing, we mock the database call
    db = connect_to_database()
    user = db.get_user(user_id)
    return user.email

def test_get_user_email():
    # Create a mock user object
    mock_user = Mock()
    mock_user.email = "test@example.com"

    # Mock the database connection and method
    with patch('my_module.connect_to_database') as mock_db:
        mock_db.return_value.get_user.return_value = mock_user
        email = get_user_email(1)
        assert email == "test@example.com"
Enter fullscreen mode Exit fullscreen mode

I used mocking in a project where my code sent emails. During testing, I didn't want to send real emails, so I mocked the email service. This made the tests faster and safer.

Integration testing checks how different parts of your code work together. It is like testing if all the pieces of a puzzle fit. You might test how a user registration process interacts with a database and an email service.

Here is an example of an integration test.

import pytest

def test_user_registration_integration():
    # Set up test components
    db = in_memory_database()
    email_sender = fake_email_sender()

    # Simulate user registration
    user_id = register_user("john@doe.com", "password", db, email_sender)

    # Check if user was saved and email sent
    assert db.has_user(user_id)
    assert email_sender.last_sent_to == "john@doe.com"
Enter fullscreen mode Exit fullscreen mode

In my experience, integration tests caught issues that unit tests missed. For example, once I had a bug where the user ID was not correctly passed between modules. Integration testing revealed the problem.

Performance testing measures how fast your code runs. It is important for making sure your application can handle real-world use. In Python, you can use the timeit module to measure execution time.

I often use this to check if my code is efficient enough.

import timeit

def slow_function():
    total = 0
    for i in range(100000):
        total += i
    return total

# Measure how long it takes
time_taken = timeit.timeit(slow_function, number=100)
print(f"It took {time_taken} seconds for 100 runs")

# In tests, you can set performance limits
def test_performance():
    time_per_run = timeit.timeit(slow_function, number=100) / 100
    assert time_per_run < 0.01  # Should take less than 10ms per run
Enter fullscreen mode Exit fullscreen mode

I had a function that processed large datasets. Performance testing showed it was too slow, so I optimized it by using better algorithms.

Security testing helps find vulnerabilities in your code. It checks for things like SQL injection or weak password handling. I always include security tests to protect user data.

Here is how you might test for SQL injection protection.

def test_sql_injection_safety():
    malicious_input = "admin' OR '1'='1"
    safe_input = sanitize_input(malicious_input)
    # Ensure the input is safe for database queries
    assert "'" not in safe_input
    assert "OR" not in safe_input  # Depending on your sanitization logic

def test_password_strength():
    weak_password = "123456"
    is_strong = check_password_strength(weak_password)
    assert is_strong == False  # Should fail for weak passwords
Enter fullscreen mode Exit fullscreen mode

In one project, security testing helped me find a flaw where user inputs were not properly sanitized. Fixing it prevented potential attacks.

Continuous integration, or CI, automates running tests whenever you change your code. It ensures that new code doesn't break existing features. I use GitHub Actions for this because it is easy to set up.

Here is a basic configuration for a Python project.

# .github/workflows/tests.yml
name: Run Tests
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout code
      uses: actions/checkout@v2
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.8'
    - name: Install dependencies
      run: pip install -r requirements.txt
    - name: Run tests
      run: pytest
Enter fullscreen mode Exit fullscreen mode

I have CI set up for all my projects. It runs tests automatically when I push code, so I know immediately if something is wrong.

Code coverage analysis shows how much of your code is tested. It helps identify parts that need more tests. I use coverage.py to generate reports.

# First, install coverage.py and run it with your tests
# coverage run -m pytest
# coverage report -m

def example_function(x):
    if x > 10:
        return "high"
    else:
        return "low"

# If you only test x=15, the else branch is not covered
def test_example_high():
    assert example_function(15) == "high"

# Missing test for x=5 would show in coverage report
Enter fullscreen mode Exit fullscreen mode

I once found that a critical error-handling branch was not tested. Coverage analysis pointed it out, and I added a test for it.

These techniques form a strong foundation for reliable software. I combine them based on the project's needs. For small scripts, I might only use unit tests. For larger applications, I include all of them.

Testing might seem extra work at first, but it pays off. I have seen projects fail because of untested code. By investing time in testing, you build confidence in your work.

I hope these examples and insights help you get started. Remember, the goal is to make your code better and your life easier. Start small, with unit tests, and gradually add more techniques as you go.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva

Top comments (0)