Introduction to Test Driven Development and Pytest
Introduction
In this lesson, we'll dive into the world of Test Driven Development (TDD) and explore the pytest framework. TDD is a crucial practice in software development that helps ensure code quality and reliability. We'll learn why TDD is important and how pytest can assist us in writing effective tests.
What is Test Driven Development (TDD)?

Test Driven Development is a development methodology where tests are written before writing the actual code. It follows a cycle of Red-Green-Refactor:
-
Red: Write a failing test for the functionality you're about to implement. This test demonstrates that your code is incomplete.
-
Green: Write the minimum amount of code necessary to make the test pass. This phase ensures that your code meets the test's requirements.
-
Refactor: Once the test passes, refactor your code to improve its structure and maintainability while keeping the tests passing.
Why TDD is Important
- Reliability: Writing tests before code ensures that your code behaves as expected, reducing the chance of bugs.
- Documentation: Tests serve as living documentation that demonstrates how your code should work.
- Maintainability: Refactoring becomes less risky when you have a comprehensive test suite.
- Collaboration: Tests allow multiple developers to work on the same codebase with confidence.
- Regression Testing: Tests catch regressions, ensuring that new changes don't break existing functionality.
Introducing Pytest
Pytest is a popular open-source testing framework for Python. It provides a simple and efficient way to write and run tests for your Python code. Pytest makes it easier to write comprehensive tests, improve code quality, and catch bugs early in the development process.
Here are some key features of Pytest:
Simplicity: Pytest uses a concise and intuitive syntax for writing tests. Test functions are just regular Python functions, and assertions are straightforward.
-
Powerful Test Discovery: Pytest automatically discovers and runs test functions in files and directories. It follows naming conventions to find and execute tests without requiring complex configuration.
-
Flexible Assertions: Pytest provides a wide range of assertion methods beyond the standard assert statement. These assertions make it easier to test various conditions and data structures.
-
Powerful Plugins: Pytest has a rich ecosystem of plugins that extend its functionality. These plugins can be used to generate test reports, integrate with continuous integration tools, and more.
-
Test Coverage Analysis: Pytest can generate coverage reports that show which parts of your code are exercised by your tests. This helps you identify areas of code that need more testing.
Adding Pytest to our Docker Containers
Pytest is essentially an addition to our Python Environment that we need to explicitly include. Luckily this will be a rather easy task to accomplish:
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir pytest
CMD ["pytest"]
The only new command we see here is the RUN command. This is different than the CMD command. RUN also executes terminal/bash based commands but can be called multiple times in order to set up your desired Environment.
Installation: Install pytest using pip:
pip install pytest #if this fails utilize pip3
Utilizing Pytest
-
Writing a Test File: In order for pytest to identify a file as a test file you must follow a specific naming convention of
test_<file>.py. -
Writing Tests: Create test functions using the naming convention
test_<function_name>. Useassertstatements to check expected outcomes which should always evaluate to a boolean.
Writing Your First Test with Pytest
Let's write a simple test using the pytest framework to check if a function works as expected:
-
Create Two Files: Create a file named
test_example.pyand another namedexample.py. -
Write the Test: In
test_example.pyimport theadd_two_numbersfunction(we have not created this function) and write a test that will assert this function can take in two numbers as arguments and return their sum.from example import add_two_numbers def test_add_two_numbers(): assert add_two_numbers(2,2) == 4 -
Run the Test: Open your terminal and navigate to the directory containing
test_example.pyandexample.py. Run the test usingpytestand watch it fail:pytest test_example.py -
Now that you've seen a test failure, lets take some time and talk about the common errors you'll encounter in
pytest:-
Assertion Errors is one of the most common errors you'll encounter in testing. It occurs when an assertion made within a test function fails. An assertion is a statement that checks whether a condition is true. If the condition is false, the AssertionError is raised, indicating that the expected behavior doesn't match the actual result.
-
Test Discovery Errors When using Pytest, it automatically discovers and runs test functions within files that match certain naming conventions. If Pytest is unable to discover test functions, you might encounter NoTestsCollected error. This can happen if your test function names do not start with "test_" or if the file names are not recognized as test files.
-
Import Errors If Pytest encounters issues importing modules or test files, you might encounter ImportError. This can happen if the required modules are not installed, if there's a typo in the module names, or if the file paths are incorrect.
-
-
Define a Function: In
example.pydefine a function that will take in two numbers as arguments and return their sum.def add_two_numbers(num_one, num_two): answer = num_one + num_two return answer -
Run the Test: Open your terminal and navigate to the directory containing
test_example.py. Run the test usingpytestto see it pass:pytest test_example.py
Assertions
Apart from assert, pytest provides other assertion methods like assertEqual, assertRaises, and more for different use cases.
Equality and Exception Assertions
- assert ==: The basic equality assertion checks if two values are equal.
assert 2 + 2 == 4
- assert !=: You can also use the inequality assertion to check if two values are not equal.
assert 3 * 5 != 11
- assertEqual: This assertion compares two values and raises an error if they are not equal. Useful for objects, lists, and more complex data structures.
assertEqual(result, expected_result)
- assertRaises: This assertion checks if a specific exception is raised when a certain action is performed.
with assertRaises(ZeroDivisionError):
result = 1 / 0
Comparison Assertions
- assert >, <, >=, <=: These assertions allow you to compare numerical values.
assert 10 > 5
assert 7 < 20
- assert math.isclose: For floating-point comparisons, you can use
math.iscloseto handle small differences due to floating-point precision.
import math
assert math.isclose(0.1 + 0.2, 0.3)
Membership and String Assertions
- assert in: You can use the membership assertion to check if a value is present in a list, tuple, or other iterable.
assert "apple" in ["apple", "banana", "cherry"]
- assert not in: Similarly, you can use the "not in" assertion to check if a value is not present in an iterable.
assert "grape" not in ["apple", "banana", "cherry"]
- assert str.startswith, str.endswith: These assertions check if a string starts with or ends with a specific substring.
assert "Hello, world!".startswith("Hello")
assert "Hello, world!".endswith("world!")
Collection Assertions
- assert len: You can use the
lenfunction to assert the length of a collection.
assert len([1, 2, 3]) == 3
- assert sorted: For testing whether a collection is sorted, you can use the
sortedfunction and compare it with the original collection.
assert sorted([3, 1, 2]) == [1, 2, 3]
Exploring Pytest's Capabilities
Monkey Patch
- Monkey Patching:You can utilize
monkeypatchfor various reasons, but in this program the main use of this tool will be to fill in arguments for our input fields within our functions.
# single input for a function
def get_user_input():
user_input = input("Enter a number: ")
return int(user_input)
def test_get_user_input(monkeypatch):
# Simulate user input
monkeypatch.setattr("builtins.input", lambda _: "42")
result = get_user_input()
assert result == 42
# multiple inputs per function
def get_multiple_inputs():
num1 = int(input("Enter the first number: "))
num2 = int(input("Enter the second number: "))
return num1 + num2
def test_get_multiple_inputs(monkeypatch):
# Simulate user inputs
user_inputs = ["5", "7"]
input_values = iter(user_inputs)
monkeypatch.setattr("builtins.input", lambda _: next(input_values))
result = get_multiple_inputs()
assert result == 5 + 7
- Capturing Terminal Output:
def test_printing(capsys):
print("Hello, pytest!")
captured = capsys.readouterr()
assert captured.out == "Hello, pytest!\n"
Conclusion
Congratulations! You've taken your first step into the world of Test Driven Development and learned about the pytest framework. Writing tests before code helps ensure the quality and reliability of your software. Remember the Red-Green-Refactor cycle, and use pytest to create and run tests effectively. This practice will greatly contribute to your skills as a Full Stack Software Engineer.