Getting Started
Running Tests and Reading Output
This lesson covers the fundamentals of invoking the test engine and interpreting the resulting feedback loop. Mastering these commands is essential for maintaining a fast development workflow and quickly diagnosing regression issues. You will use these skills every time you need to verify code integrity or isolate specific failure points during local development.
Executing a Basic Test Suite
To begin running tests, you invoke the command in your terminal. When you simply execute the command without arguments, the engine recursively searches the current directory and subdirectories for files starting with 'test_' or ending with '_test.py'. Once these files are identified, the runner scans them for functions prefixed with 'test_' and executes them. This automatic discovery mechanism relies on standard naming conventions, which eliminates the need for manual configuration files. The engine acts as a discovery service that inspects the module namespace, imports your code, and runs the identified functions sequentially. By understanding that this is a dynamic process rather than a static one, you can reason that adding a new test file simply requires following the naming pattern for it to be integrated automatically into your existing test suite execution without any additional setup or registration steps required by the developer.
# Save as test_math.py
# Run with: pytest test_math.py
def test_addition():
# The runner executes functions starting with 'test_'
assert 1 + 1 == 2
def test_subtraction():
# Each assert failure will stop execution of that specific function
assert 5 - 2 == 3Interpreting Test Results and Indicators
When your suite finishes execution, the output provides a visual summary composed of specific symbols. A dot represents a successful test, while an 'F' indicates a failure, an 'E' denotes an error during setup or teardown, and an 's' signifies a skipped test. These symbols represent the internal state of each test case after completion. A failure occurs when an assertion statement evaluates to False, whereas an error typically happens when an unexpected exception is raised outside of an assertion, often due to faulty test setup or environment issues. The runner uses these indicators to provide immediate high-level feedback on the health of your codebase. By observing this status line, you can quickly assess the scale of a regression. If you see many 'E' symbols, your infrastructure or fixtures likely have issues, while 'F' symbols point specifically to logic errors within your application code or test assertions.
# Save as test_status.py
# Run with: pytest -v test_status.py
def test_pass():
assert True # Shows as '.'
def test_fail():
assert 1 == 2 # Shows as 'F'
def test_error():
raise ValueError('Setup issue') # Shows as 'E'Analyzing Assertion Failures
When a test fails, the engine provides a detailed 'AssertionError' traceback. The real power of the output lies in the 'assert' introspection. Instead of just showing that an assertion failed, the output decomposes the expression to show the specific values that caused the discrepancy. For instance, if you compare two lists or dictionaries, the runner highlights exactly which elements differ, making it much easier to debug complex data structures. This works by inspecting the abstract syntax tree of your assertion expression during runtime, capturing the values of the components before the final comparison fails. Because the engine interprets the assertion directly, you do not need to manually write detailed error messages for every check. Relying on this native introspection allows you to write concise, readable test code while still receiving highly descriptive failure reports that pinpoint the exact source of logic bugs.
# Save as test_analysis.py
# Run with: pytest test_analysis.py
def test_list_comparison():
expected = [1, 2, 3]
actual = [1, 2, 4]
# The engine will show the diff between 3 and 4
assert actual == expectedTargeting Specific Tests and Modules
Often, you will not want to run the entire suite, especially as your project grows. You can target specific files, directories, or even individual test functions by providing their names as arguments to the runner. By passing a path to a specific file, you isolate the scope, ensuring that only tests relevant to your current task are executed. Furthermore, you can use the '-k' flag to run tests that match a specific substring, which is invaluable when you have multiple tests related to a single feature but spread across various files. This filtering mechanism works by applying a regex-like match against the collection of identified test names. Understanding this allows you to reason about how to build custom workflows, such as running only the 'login' tests before pushing your code to production. It optimizes your feedback loop, allowing you to focus on specific logic without waiting for the entire project's validation process to conclude.
# Run only the login file:
# pytest test_auth.py
# Run only tests containing 'signup':
# pytest -k "signup"
def test_user_signup():
assert True
def test_admin_login():
assert TrueControlling Verbosity and Output
The default output provides a minimal summary, but sometimes you need more detail to understand exactly what is happening during a test run. The '-v' (verbose) flag increases the detail level, listing every individual test function that was executed alongside its status. Adding '-s' is another common requirement; it allows you to see the output from 'print' statements during test execution, which are captured and suppressed by default to keep the console clean. These flags are not just for display; they change how the runner manages standard streams and reports progress. By combining these flags, you can tailor the level of diagnostic information to match your debugging needs. Learning to control the output format ensures that you remain productive whether you need a quick 'pass/fail' summary or a deep dive into the internal state of a failing integration test that requires logging output for visibility.
# Run with verbose output and visible print statements:
# pytest -v -s test_verbose.py
def test_data_logging():
data = {'key': 'value'}
print(f"Debugging: {data}") # Visible only with -s
assert data['key'] == 'value'Key points
- Tests are discovered automatically based on the 'test_' prefix naming convention.
- The test runner provides specific symbols to represent pass, fail, error, and skip states.
- Assertion introspection automatically decomposes expressions to highlight discrepancies in data.
- Targeted execution can be achieved by passing filenames or using the '-k' flag for filtering.
- Tracebacks serve as the primary diagnostic tool for locating the source of logic errors.
- The '-v' flag increases verbosity to show detailed results for every individual test function.
- Captured standard output can be displayed in the terminal using the '-s' command-line flag.
- Understanding the discovery and execution cycle allows for efficient custom workflow development.
Common mistakes
- Mistake: Relying on print statements to debug tests. Why it's wrong: pytest captures stdout by default, so you won't see your debug output unless a test fails or you use specific flags. Fix: Use logging or the -s flag to disable stdout capturing.
- Mistake: Naming test files or functions without the 'test_' prefix. Why it's wrong: pytest uses discovery rules that only collect files/functions starting with 'test_'. Fix: Rename files to test_*.py and functions to test_*().
- Mistake: Assuming tests run in a specific order. Why it's wrong: pytest runs tests in an order based on filesystem and internal heuristics, not the order in the file. Fix: Write tests to be isolated and independent of each other.
- Mistake: Ignoring test failure output without reading the traceback. Why it's wrong: The traceback contains exactly which assertion failed and the current state of variables. Fix: Use the --tb=short or --tb=long flags to control the verbosity of the failure output.
- Mistake: Writing large, monolithic tests that cover multiple features. Why it's wrong: When one assertion fails, the rest of the test code is never executed, making it hard to identify secondary bugs. Fix: Keep tests granular with one distinct assertion focus per test.
Interview questions
What is the most basic command to run your tests in a pytest project, and how does it locate the files?
To run your tests, you simply execute the 'pytest' command in your terminal from the root directory of your project. Pytest automatically discovers test files by looking for any file prefixed with 'test_' or suffixed with '_test.py'. Within those files, it recursively searches for functions or classes that also start with the 'test_' prefix. This convention-over-configuration approach is highly efficient because it removes the need for manual test registration, allowing developers to focus entirely on writing test logic while pytest handles the automated discovery process across the entire repository.
How can you interpret the output when a test fails, and what do the dots, Fs, and Es represent?
When you run pytest, the console output provides a summary of the execution status using specific characters. A dot (.) signifies a passed test, an 'F' indicates a test failure—meaning an assertion failed—and an 'E' denotes an error, usually occurring outside the test logic, such as an issue with a fixture setup or a syntax error. When a failure occurs, pytest prints a detailed 'traceback' showing exactly which line failed, the expected versus actual values, and the state of local variables. This output is critical for debugging because it isolates the point of failure immediately.
How can you use verbose mode or specific markers to control which tests are executed?
You can use the '-v' flag for verbose mode, which prints the full name of every test instead of just a dot, providing better visibility into the test suite's progress. To filter tests, you can use the '-m' flag followed by a marker name, such as '@pytest.mark.smoke'. This allows you to selectively run only specific subsets of your test suite. Using markers is essential for larger projects where you want to run quick sanity checks without executing the entire slow-running integration test suite, thereby significantly improving developer feedback loops.
Compare running tests using 'pytest -x' versus 'pytest --maxfail=N'. When would you choose one over the other?
The '-x' flag, or 'exitfirst', tells pytest to stop execution immediately after the very first failure encountered. This is ideal when you are actively debugging a specific issue and don't want to wait for the rest of the suite to run if the code is fundamentally broken. Conversely, '--maxfail=N' allows the suite to continue until a specific number of failures occur. You would choose this when you need a broader overview of how many tests are failing, which helps in identifying if a failure is isolated to one module or represents a widespread regression across the codebase.
What is the role of the 'pytest --durations=N' flag, and why is it useful in a professional CI/CD environment?
The '--durations=N' flag instructs pytest to display the execution times of the N slowest tests in your suite after the run completes. In a professional CI/CD pipeline, this is an invaluable tool for performance monitoring. By identifying bottlenecks, you can focus your optimization efforts where they provide the most value, such as refactoring slow database-heavy tests or mocking external network calls. This proactive management prevents the test suite from becoming a productivity drag, ensuring that feedback remains fast even as the project scales.
How can you capture and inspect standard output (stdout) or logging during a failed test run in pytest?
By default, pytest captures stdout and stderr and only displays them if a test fails. You can force pytest to show this output during passes using the '-s' flag. However, for debugging failures, the captured output is automatically included in the failure report. To see more detailed logs, you can use the '--log-cli-level' command. For example, 'pytest --log-cli-level=DEBUG' will output all log statements to the console. This is essential because it allows you to trace the execution flow of your code alongside the failed assertions, providing the necessary context to understand why a test did not behave as expected.
Check yourself
1. If your test output is missing print statements you included for debugging, what is the most direct way to see them in the console?
- A.Use the --verbose flag
- B.Use the -s flag
- C.Use the -v flag
- D.Use the --collect-only flag
Show answer
B. Use the -s flag
The -s flag allows output to be printed to stdout. --verbose and -v increase detail but do not disable capture, and --collect-only just lists tests without running them.
2. Why does pytest not execute a function named 'check_login_functionality' even if it contains valid assertions?
- A.It lacks a return statement
- B.It needs to be inside a class
- C.It does not start with the 'test_' prefix
- D.It is not marked as a test
Show answer
C. It does not start with the 'test_' prefix
Pytest uses test discovery based on the prefix 'test_'. Changing the name to 'test_check_login_functionality' would allow pytest to find it automatically.
3. What happens when an assertion fails midway through a test function?
- A.The test is marked as skipped
- B.The test is marked as errored
- C.The execution stops immediately, and the remainder of the test is skipped
- D.The test continues to the next assertion
Show answer
C. The execution stops immediately, and the remainder of the test is skipped
Pytest treats an assertion failure as a failure that terminates the execution of that specific test function immediately. It is not skipped or errored; it is failed.
4. How can you run only a specific subset of tests defined by a keyword in their name?
- A.Using the -k flag
- B.Using the -m flag
- C.Using the --collect-only flag
- D.Using the -n flag
Show answer
A. Using the -k flag
The -k flag allows filtering tests by matching substrings in their names. -m is for markers, -n is for parallelization, and --collect-only is for inspection.
5. Which of the following describes the best practice for reading test failure output?
- A.Read only the first line of the output
- B.Ignore the traceback and focus on the summary line
- C.Look for the assertion failure details in the 'FAILURES' section of the output
- D.Restart the suite with the --force-regen flag
Show answer
C. Look for the assertion failure details in the 'FAILURES' section of the output
The 'FAILURES' section provides the specific line where the assertion failed and the values of the variables involved, which is essential for debugging. The other options ignore critical diagnostic data.