Chapter 11: Testing Your Code
|
Source: Python Crash Course, 3rd Edition by Eric Matthes |
When you write a function or a class, you can also write tests for that code. Testing proves that your code works as it’s supposed to in response to all the kinds of input it’s designed to receive. When you write tests, you can be confident that your code will work correctly as more people begin to use your programs. You’ll also be able to test new code as you add it, to make sure your changes don’t break your program’s existing behavior. Every programmer makes mistakes, so every programmer must test their code often, to catch problems before users encounter them.
In this chapter, you’ll learn to test your code using pytest. The
pytest library is a collection of tools that will help you write your
first tests quickly and simply, while supporting your tests as they grow
in complexity along with your projects. Python doesn’t include pytest
by default, so you’ll learn to install external libraries. Knowing how
to install external libraries will make a wide variety of well-designed
code available to you. These libraries will expand the kinds of projects
you can work on immensely.
You’ll learn to build a series of tests and check that each set of inputs results in the output you want. You’ll see what a passing test looks like and what a failing test looks like, and you’ll learn how a failing test can help you improve your code. You’ll learn to test functions and classes, and you’ll start to understand how many tests to write for a project.
Installing pytest with pip
While Python includes a lot of functionality in the standard library, Python developers also depend heavily on third-party packages. A third-party package is a library that’s developed outside the core Python language. Some popular third-party libraries are eventually adopted into the standard library, and end up being included in most Python installations from that point forward.
Many packages, however, are kept out of the standard library so they can
be developed on a timeline independent of the language itself. These
packages tend to be updated more frequently than they would be if they
were tied to Python’s development schedule. This is true of pytest and
most of the libraries we’ll use in the second half of this book. You
shouldn’t blindly trust every third-party package, but you also
shouldn’t be put off by the fact that a lot of important functionality
is implemented through such packages.
Updating pip
Python includes a tool called pip that’s used to install third-party
packages. Because pip helps install packages from external resources,
it’s updated often to address potential security issues. So, we’ll start
by updating pip.
Open a new terminal window and issue the following command:
$ python -m pip install --upgrade pip
Requirement already satisfied: pip in /.../python3.11/site-packages (22.0.4) (1)
--snip--
Successfully installed pip-22.1.2 (2)
| 1 | The current version of pip, version 22.0.4, was found already
installed. |
| 2 | The latest version at the time of this writing, 22.1.2, was installed. |
The first part of this command, python -m pip, tells Python to run the
module pip. The second part, install --upgrade, tells pip to
update a package that’s already been installed. The last part, pip,
specifies which third-party package should be updated.
You can use this command to update any third-party package installed on your system:
$ python -m pip install --upgrade package_name
|
If you’re using Linux, |
Installing pytest
Now that pip is up to date, we can install pytest:
$ python -m pip install --user pytest
Collecting pytest
--snip--
Successfully installed attrs-21.4.0 iniconfig-1.1.1 ...pytest-7.x.x
We’re still using the core command pip install, without the
--upgrade flag this time. Instead, we’re using the --user flag,
which tells Python to install this package for the current user only.
The output shows that the latest version of pytest was successfully
installed, along with a number of other packages that pytest depends
on.
You can use this command to install many third-party packages:
$ python -m pip install --user package_name
|
If you have any difficulty running this command, try running the same
command without the |
Testing a Function
To learn about testing, we need code to test. Here’s a simple function that takes in a first and last name, and returns a neatly formatted full name:
def get_formatted_name(first, last):
"""Generate a neatly formatted full name."""
full_name = f"{first} {last}"
return full_name.title()
The function get_formatted_name() combines the first and last name
with a space in between to complete a full name, and then capitalizes
and returns the full name. To check that get_formatted_name() works,
let’s make a program that uses this function. The program names.py
lets users enter a first and last name, and see a neatly formatted full
name:
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == 'q':
break
last = input("Please give me a last name: ")
if last == 'q':
break
formatted_name = get_formatted_name(first, last)
print(f"\tNeatly formatted name: {formatted_name}.")
This program imports get_formatted_name() from name_function.py. The
user can enter a series of first and last names and see the formatted
full names that are generated:
Enter 'q' at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin.
Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan.
Please give me a first name: q
We can see that the names generated here are correct. But say we want to
modify get_formatted_name() so it can also handle middle names. As we
do so, we want to make sure we don’t break the way the function handles
names that have only a first and last name. We could test our code by
running names.py and entering a name like Janis Joplin every time we
modify get_formatted_name(), but that would become tedious.
Fortunately, pytest provides an efficient way to automate the testing
of a function’s output. If we automate the testing of
get_formatted_name(), we can always be confident that the function
will work when given the kinds of names we’ve written tests for.
Unit Tests and Test Cases
There is a wide variety of approaches to testing software. One of the simplest kinds of test is a unit test. A unit test verifies that one specific aspect of a function’s behavior is correct. A test case is a collection of unit tests that together prove that a function behaves as it’s supposed to, within the full range of situations you expect it to handle.
A good test case considers all the possible kinds of input a function could receive and includes tests to represent each of these situations. A test case with full coverage includes a full range of unit tests covering all the possible ways you can use a function. Achieving full coverage on a large project can be daunting. It’s often good enough to write tests for your code’s critical behaviors and then aim for full coverage only if the project starts to see widespread use.
A Passing Test
With pytest, writing your first unit test is pretty straightforward.
We’ll write a single test function. The test function will call the
function we’re testing, and we’ll make an assertion about the value
that’s returned. If our assertion is correct, the test will pass; if the
assertion is incorrect, the test will fail.
Here’s the first test of the function get_formatted_name():
from name_function import get_formatted_name
def test_first_last_name(): (1)
"""Do names like 'Janis Joplin' work?"""
formatted_name = get_formatted_name('janis', 'joplin') (2)
assert formatted_name == 'Janis Joplin' (3)
| 1 | The name of a test file is important; it must start with test_.
When we ask pytest to run the tests we’ve written, it will look
for any file that begins with test_, and run all of the tests it
finds in that file. Any function that starts with test_ will be
discovered by pytest and run as part of the testing process. |
| 2 | We call the function we’re testing with the arguments 'janis' and
'joplin', just like we used when we ran names.py. We assign the
return value of this function to formatted_name. |
| 3 | We make an assertion. An assertion is a claim about a condition.
Here we’re claiming that the value of formatted_name should be
'Janis Joplin'. |
Test function names should be long enough that if you see the function name in a test report, you’ll have a good sense of what behavior was being tested.
Running a Test
If you run the file test_name_function.py directly, you won’t get any
output because we never called the test function. Instead, we’ll have
pytest run the test file for us.
To do this, open a terminal window and navigate to the folder that
contains the test file. If you’re using VS Code, you can open the folder
containing the test file and use the terminal that’s embedded in the
editor window. In the terminal window, enter the command pytest. Here’s
what you should see:
$ pytest
========================= test session starts =========================
platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x (1)
rootdir: /.../python_work/chapter_11 (2)
collected 1 item (3)
test_name_function.py . [100%] (4)
========================== 1 passed in 0.00s ==========================
| 1 | We can see which versions of Python, pytest, and other packages
are being used to run the test. |
| 2 | We can see the directory where the test is being run from. |
| 3 | pytest found one test to run. |
| 4 | The single dot after the name of the file tells us that a single test passed, and the 100% makes it clear that all of the tests have been run. |
The last line tells us that one test passed, and it took less than 0.01 seconds to run the test.
This output indicates that the function get_formatted_name() will
always work for names that have a first and last name, unless we modify
the function. When we modify get_formatted_name(), we can run this
test again. If the test passes, we know the function will still work for
names like Janis Joplin.
|
If you’re not sure how to navigate to the right location in the
terminal, see Running Python Programs from a Terminal on page 11.
Also, if you see a message that the |
A Failing Test
What does a failing test look like? Let’s modify get_formatted_name()
so it can handle middle names, but let’s do so in a way that breaks the
function for names with just a first and last name, like Janis Joplin.
Here’s a new version of get_formatted_name() that requires a middle
name argument:
def get_formatted_name(first, middle, last):
"""Generate a neatly formatted full name."""
full_name = f"{first} {middle} {last}"
return full_name.title()
This version should work for people with middle names, but when we test
it, we see that we’ve broken the function for people with just a first
and last name. This time, running pytest gives the following output:
$ pytest
========================= test session starts =========================
--snip--
test_name_function.py F [100%] (1)
============================== FAILURES =============================== (2)
________________________ test_first_last_name _________________________ (3)
def test_first_last_name():
"""Do names like 'Janis Joplin' work?"""
> formatted_name = get_formatted_name('janis', 'joplin') (4)
E TypeError: get_formatted_name() missing 1 required positional argument: 'last' (5)
test_name_function.py:5: TypeError
======================= short test summary info =======================
FAILED test_name_function.py::test_first_last_name - TypeError:
get_formatted_name() missing 1 required positional argument: 'last'
========================== 1 failed in 0.04s ==========================
| 1 | A single F tells us that one test failed. |
| 2 | The FAILURES section focuses on what failed tests are usually the
most important thing to focus on in a test run. |
| 3 | test_first_last_name() was the test function that failed. |
| 4 | An angle bracket indicates the line of code that caused the test to fail. |
| 5 | The E shows the actual error that caused the failure: a TypeError
due to a missing required positional argument, last. |
The most important information is repeated in a shorter summary at the end, so when you’re running many tests, you can get a quick sense of which tests failed and why.
Responding to a Failed Test
What do you do when a test fails? Assuming you’re checking the right conditions, a passing test means the function is behaving correctly and a failing test means there’s an error in the new code you wrote. So when a test fails, don’t change the test. If you do, your tests might pass, but any code that calls your function like the test does will suddenly stop working. Instead, fix the code that’s causing the test to fail. Examine the changes you just made to the function, and figure out how those changes broke the desired behavior.
In this case, get_formatted_name() used to require only two
parameters: a first name and a last name. Now it requires a first name,
middle name, and last name. The addition of that mandatory middle name
parameter broke the original behavior of get_formatted_name(). The
best option here is to make the middle name optional. Once we do, our
test for names like Janis Joplin should pass again, and we should be
able to accept middle names as well. Let’s modify
get_formatted_name() so middle names are optional and then run the
test case again:
def get_formatted_name(first, last, middle=''):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
In this new version of get_formatted_name(), the middle name is
optional. If a middle name is passed to the function, the full name will
contain a first, middle, and last name. Otherwise, the full name will
consist of just a first and last name. Now the function should work for
both kinds of names. To find out if the function still works for names
like Janis Joplin, let’s run the test again:
$ pytest
========================= test session starts =========================
--snip--
test_name_function.py . [100%]
========================== 1 passed in 0.00s ==========================
The test passes now. This is ideal; it means the function works for names like Janis Joplin again, without us having to test the function manually. Fixing our function was easier because the failed test helped us identify how the new code broke existing behavior.
Adding New Tests
Now that we know get_formatted_name() works for simple names again,
let’s write a second test for people who include a middle name. We do
this by adding another test function to the file
test_name_function.py:
from name_function import get_formatted_name
def test_first_last_name():
# --snip--
def test_first_last_middle_name():
"""Do names like 'Wolfgang Amadeus Mozart' work?"""
formatted_name = get_formatted_name( (1)
'wolfgang', 'mozart', 'amadeus')
assert formatted_name == 'Wolfgang Amadeus Mozart' (2)
| 1 | We call get_formatted_name() with a first, last, and middle name. |
| 2 | We make an assertion that the returned full name matches the full name (first, middle, and last) that we expect. |
We name this new function test_first_last_middle_name(). The function
name must start with test_ so the function runs automatically when we
run pytest. We name the function to make it clear which behavior of
get_formatted_name() we’re testing. As a result, if the test fails,
we’ll know right away what kinds of names are affected.
When we run pytest again, both tests pass:
$ pytest
========================= test session starts =========================
--snip--
collected 2 items
test_name_function.py .. [100%] (1)
========================== 2 passed in 0.01s ==========================
| 1 | The two dots indicate that two tests passed. |
We now know that the function still works for names like Janis Joplin, and we can be confident that it will work for names like Wolfgang Amadeus Mozart as well.
Try It Yourself
11-1. City, Country: Write a function that accepts two parameters: a
city name and a country name. The function should return a single string
of the form City, Country, such as Santiago, Chile. Store the
function in a module called city_functions.py, and save this file in a
new folder so pytest won’t try to run the tests we’ve already written.
Create a file called test_cities.py that tests the function you just
wrote. Write a function called test_city_country() to verify that
calling your function with values such as 'santiago' and 'chile'
results in the correct string. Run the test, and make sure
test_city_country() passes.
11-2. Population: Modify your function so it requires a third
parameter, population. It should now return a single string of the
form City, Country - population xxx, such as
Santiago, Chile - population 5000000. Run the test again, and make
sure test_city_country() fails this time. Modify the function so the
population parameter is optional. Run the test, and make sure
test_city_country() passes again. Write a second test called
test_city_country_population() that verifies you can call your
function with the values 'santiago', 'chile', and
'population=5000000'. Run the tests one more time, and make sure this
new test passes.
Testing a Class
In the first part of this chapter, you wrote tests for a single function. Now you’ll write tests for a class. You’ll use classes in many of your own programs, so it’s helpful to be able to prove that your classes work correctly. If you have passing tests for a class you’re working on, you can be confident that improvements you make to the class won’t accidentally break its current behavior.
A Variety of Assertions
So far, you’ve seen just one kind of assertion: a claim that a string
has a specific value. When writing a test, you can make any claim that
can be expressed as a conditional statement. If the condition is True
as expected, your assumption about how that part of your program behaves
will be confirmed; you can be confident that no errors exist. If the
condition you assume is True is actually False, the test will fail
and you’ll know there’s an issue to resolve.
| Assertion | Claim |
|---|---|
|
Assert that two values are equal. |
|
Assert that two values are not equal. |
|
Assert that |
|
Assert that |
|
Assert that an element is in a list. |
|
Assert that an element is not in a list. |
These are just a few examples; anything that can be expressed as a conditional statement can be included in a test.
A Class to Test
Testing a class is similar to testing a function, because much of the work involves testing the behavior of the methods in the class. However, there are a few differences, so let’s write a class to test. Consider a class that helps administer anonymous surveys:
class AnonymousSurvey:
"""Collect anonymous answers to a survey question."""
def __init__(self, question): (1)
"""Store a question, and prepare to store responses."""
self.question = question
self.responses = []
def show_question(self): (2)
"""Show the survey question."""
print(self.question)
def store_response(self, new_response): (3)
"""Store a single response to the survey."""
self.responses.append(new_response)
def show_results(self): (4)
"""Show all the responses that have been given."""
print("Survey results:")
for response in self.responses:
print(f"- {response}")
| 1 | This class starts with a survey question that you provide and includes an empty list to store responses. |
| 2 | The class has a method to print the survey question. |
| 3 | A method to add a new response to the response list. |
| 4 | A method to print all the responses stored in the list. |
To show that the AnonymousSurvey class works, let’s write a program
that uses the class:
from survey import AnonymousSurvey
# Define a question, and make a survey.
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
# Show the question, and store responses to the question.
language_survey.show_question()
print("Enter 'q' at any time to quit.")
while True:
response = input("Language: ")
if response == 'q':
break
language_survey.store_response(response)
# Show the survey results.
print("\nThank you to everyone who participated in the survey!")
language_survey.show_results()
This program defines a question and creates an AnonymousSurvey object
with that question. The program calls show_question() to display the
question and then prompts for responses. Each response is stored as it
is received:
What language did you first learn to speak?
Enter 'q' at any time to quit.
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
This class works for a simple anonymous survey, but say we want to
improve AnonymousSurvey and the module it’s in. We could allow each
user to enter more than one response, we could write a method to list
only unique responses and to report how many times each response was
given, or we could even write another class to manage non-anonymous
surveys. Implementing such changes would risk affecting the current
behavior of the class AnonymousSurvey. To ensure we don’t break
existing behavior as we develop this module, we can write tests for the
class.
Testing the AnonymousSurvey Class
Let’s write a test that verifies one aspect of the way AnonymousSurvey
behaves. We’ll write a test to verify that a single response to the
survey question is stored properly:
from survey import AnonymousSurvey
def test_store_single_response(): (1)
"""Test that a single response is stored properly."""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question) (2)
language_survey.store_response('English')
assert 'English' in language_survey.responses (3)
| 1 | A good descriptive name for this function is
test_store_single_response(). If this test fails, we’ll know from
the function name in the test summary that there was a problem
storing a single response to the survey. |
| 2 | To test the behavior of a class, we need to make an instance of the
class. We create an instance called language_survey with the
question "What language did you first learn to speak?" and store a
single response, English, using the store_response() method. |
| 3 | We verify that the response was stored correctly by asserting that
English is in the list language_survey.responses. |
By default, running the command pytest with no arguments will run all
the tests that pytest discovers in the current directory. To focus on
the tests in one file, pass the name of the test file you want to run.
Here we’ll run just the one test we wrote for AnonymousSurvey:
$ pytest test_survey.py
========================= test session starts =========================
--snip--
test_survey.py . [100%]
========================== 1 passed in 0.01s ==========================
This is a good start, but a survey is useful only if it generates more
than one response. Let’s verify that three responses can be stored
correctly. To do this, we add another function to test_survey.py:
from survey import AnonymousSurvey
def test_store_single_response():
# --snip--
def test_store_three_responses():
"""Test that three individual responses are stored properly."""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
responses = ['English', 'Spanish', 'Mandarin'] (1)
for response in responses:
language_survey.store_response(response)
for response in responses: (2)
assert response in language_survey.responses
| 1 | We define a list containing three different responses, and then call
store_response() for each of these responses. |
| 2 | Once the responses have been stored, we write another loop and assert
that each response is now in language_survey.responses. |
When we run the test file again, both tests (for a single response and for three responses) pass:
$ pytest test_survey.py
========================= test session starts =========================
--snip--
test_survey.py .. [100%]
========================== 2 passed in 0.01s ==========================
This works perfectly. However, these tests are a bit repetitive, so
we’ll use another feature of pytest to make them more efficient.
Using Fixtures
In test_survey.py, we created a new instance of AnonymousSurvey in
each test function. This is fine in the short example we’re working
with, but in a real-world project with tens or hundreds of tests, this
would be problematic.
In testing, a fixture helps set up a test environment. Often, this
means creating a resource that’s used by more than one test. We create a
fixture in pytest by writing a function with the decorator
@pytest.fixture. A decorator is a directive placed just before a
function definition; Python applies this directive to the function
before it runs, to alter how the function code behaves. Don’t worry if
this sounds complicated; you can start to use decorators from
third-party packages before learning to write them yourself.
Let’s use a fixture to create a single survey instance that can be used
in both test functions in test_survey.py:
import pytest
from survey import AnonymousSurvey
@pytest.fixture (1)
def language_survey(): (2)
"""A survey that will be available to all test functions."""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
return language_survey
def test_store_single_response(language_survey): (3)
"""Test that a single response is stored properly."""
language_survey.store_response('English') (4)
assert 'English' in language_survey.responses
def test_store_three_responses(language_survey): (5)
"""Test that three individual responses are stored properly."""
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
language_survey.store_response(response) (6)
for response in responses:
assert response in language_survey.responses
| 1 | We apply the @pytest.fixture decorator to the new function
language_survey(). |
| 2 | This function builds an AnonymousSurvey object and returns the new
survey. |
| 3 | Notice that the definition of test_store_single_response() has
changed: it now has a parameter called language_survey. When a
parameter in a test function matches the name of a function with the
@pytest.fixture decorator, the fixture will be run automatically
and the return value will be passed to the test function. |
| 4 | Two lines have been removed from this function: the line that defined
a question and the line that created an AnonymousSurvey object. |
| 5 | The definition of test_store_three_responses() has changed as
well: it now also has a parameter called language_survey. |
| 6 | Two lines have been removed from this function as well. |
We need to import pytest now, because we’re using a decorator that’s
defined in pytest.
When we run the test file again, both tests still pass. These tests
would be particularly useful when trying to expand AnonymousSurvey to
handle multiple responses for each person. After modifying the code to
accept multiple responses, you could run these tests and make sure you
haven’t affected the ability to store a single response or a series of
individual responses.
The structure above will almost certainly look complicated; it contains some of the most abstract code you’ve seen so far. You don’t need to use fixtures right away; it’s better to write tests that have a lot of repetitive code than to write no tests at all. Just know that when you’ve written enough tests that the repetition is getting in the way, there’s a well-established way to deal with the repetition. Also, fixtures in simple examples like this one don’t really make the code any shorter or simpler to follow. But in projects with many tests, or in situations where it takes many lines to build a resource that’s used in multiple tests, fixtures can drastically improve your test code.
When you want to write a fixture, write a function that generates the
resource that’s used by multiple test functions. Add the @pytest.fixture
decorator to the new function, and add the name of this function as a
parameter for each test function that uses this resource. Your tests
will be shorter and easier to write and maintain from that point
forward.
Try It Yourself
11-3. Employee: Write a class called Employee. The init()
method should take in a first name, a last name, and an annual salary,
and store each of these as attributes. Write a method called
give_raise() that adds $5,000 to the annual salary by default but also
accepts a different raise amount. Write a test file for Employee with
two test functions, test_give_default_raise() and
test_give_custom_raise(). Write your tests once without using a
fixture, and make sure they both pass. Then write a fixture so you don’t
have to create a new employee instance in each test function. Run the
tests again, and make sure both tests still pass.
Summary
In this chapter, you learned to write tests for functions and classes
using tools in the pytest module. You learned to write test functions
that verify specific behaviors your functions and classes should
exhibit. You saw how fixtures can be used to efficiently create
resources that can be used in multiple test functions in a test file.
Testing is an important topic that many newer programmers aren’t exposed to. You don’t have to write tests for all the simple projects you try as a new programmer. But as soon as you start to work on projects that involve significant development effort, you should test the critical behaviors of your functions and classes. You’ll be more confident that new work on your project won’t break the parts that work, and this will give you the freedom to make improvements to your code. If you accidentally break existing functionality, you’ll know right away, so you can still fix the problem easily. Responding to a failed test that you ran is much easier than responding to a bug report from an unhappy user.
Other programmers will respect your projects more if you include some initial tests. They’ll feel more comfortable experimenting with your code and be more willing to work with you on projects. If you want to contribute to a project that other programmers are working on, you’ll be expected to show that your code passes existing tests and you’ll usually be expected to write tests for any new behavior you introduce to the project.
Play around with tests to become familiar with the process of testing your code. Write tests for the most critical behaviors of your functions and classes, but don’t aim for full coverage in early projects unless you have a specific reason to do so.
Applied Exercises: Ch 11 — Testing Your Code
These exercises cover the same concepts as the chapter but use context
from real infrastructure, network security, and language learning work.
Save each as a separate .py file using lowercase and underscores, e.g.
test_ise_utils.py.
Domus Digitalis / Homelab
D11-1. Node Formatter Function: Write a function called
get_node_summary(hostname, role, ip) in a module called
node_utils.py. The function should return a formatted string like
kvm-01 (hypervisor) — 10.100.0.10. Write a test file
test_node_utils.py with at least two test functions: one that tests
the basic three-parameter call, and one that verifies the output
contains the hostname.
D11-2. VLAN Lookup Function: Write a function called
get_vlan_name(vlan_id) in a module called vlan_utils.py. The
function should look up VLAN IDs from a dictionary and return the name,
or 'Unknown VLAN' if the ID isn’t found. Write a test file
test_vlan_utils.py that tests at least three VLAN IDs: one that exists
in the dictionary, one that doesn’t, and one edge case.
D11-3. Service Status Class: Write a class called ServiceMonitor in
service_monitor.py. The init() method should accept a name
attribute. Add a method record_event(event) that appends the event to
a self.events list. Write a test file test_service_monitor.py with
two test functions — one that tests storing a single event, and one that
tests storing three events — first without a fixture, then refactored to
use a @pytest.fixture.
D11-4. BGP Peer Validator: Write a function called
is_valid_peer(asn, tunnel_ip) in a module called bgp_utils.py. The
function should return True if the ASN is a positive integer and the
tunnel IP is a non-empty string, and False otherwise. Write a test
file test_bgp_utils.py that tests at least four cases: a valid peer,
an invalid ASN, an empty tunnel IP, and both invalid.
D11-5. Node Fixture Test: Write a class called Node in
domus_nodes.py with attributes hostname, role, and a status
defaulting to 'up'. Add methods mark_down() and mark_up() that
change the status. Write a test file test_domus_nodes.py that uses a
@pytest.fixture to create a shared Node instance. Write at least two
test functions that verify mark_down() and mark_up() work correctly.
CHLA / ISE / Network Security
C11-1. Policy Formatter Function: Write a function called
format_policy_entry(name, protocol, result='Allow') in a module
called ise_utils.py. The function should return a formatted string
like 802.1X Wired | EAP-TLS | Allow. Write a test file
test_ise_utils.py with at least two test functions: one that tests
the call with all three arguments, and one that tests the default
result value.
C11-2. Syslog Severity Lookup: Write a function called
get_severity_label(level) in syslog_utils.py. The function should
return the severity label string for levels 0–7, or 'Invalid' for
any other value. Write a test file test_syslog_utils.py that tests at
least five severity levels: 0, 3, 6, 7, and an out-of-range value.
C11-3. Endpoint Registry Class: Write a class called
EndpointRegistry in endpoint_registry.py. The init() method
should initialize an empty self.endpoints list. Add a method
register(mac_address) that appends the MAC address to the list. Write
a test file test_endpoint_registry.py with two test functions — one
for registering a single endpoint and one for registering three — first
without a fixture, then refactored to use @pytest.fixture.
C11-4. Pipeline Stage Validator: Write a function called
is_valid_stage(name, log_types) in pipeline_utils.py. The function
should return True if name is a non-empty string and log_types is
a non-empty list, and False otherwise. Write a test file
test_pipeline_utils.py that tests at least four cases: a valid stage,
an empty name, an empty log_types list, and both invalid.
C11-5. Survey Fixture Pattern Applied to ISE: Write a class called
ISEAuditLog in ise_audit.py. The init() method should accept
a node attribute and initialize an empty self.entries list. Add a
method log_event(event) that appends the event string to entries.
Write a test file test_ise_audit.py that uses a @pytest.fixture to
create a shared ISEAuditLog instance. Write at least two test
functions verifying that single and multiple events are logged correctly.
General Sysadmin / Linux
L11-1. Service Name Formatter: Write a function called
format_service_status(name, state, host) in sysadmin_utils.py. The
function should return a formatted string like
wazuh-manager: active on kvm-01. Write a test file
test_sysadmin_utils.py with at least two test functions.
L11-2. Package Version Checker: Write a function called
is_version_current(installed, required) in pkg_utils.py. The
function should return True if the installed version string matches
the required version string, and False otherwise. Write a test file
test_pkg_utils.py that tests at least three cases: matching versions,
mismatched versions, and an empty installed version.
L11-3. Cron Job Registry Class: Write a class called CronRegistry in
cron_registry.py. The init() method should initialize an empty
self.jobs list. Add a method add_job(name, schedule) that appends a
dictionary to self.jobs. Write a test file test_cron_registry.py
with two test functions — one for adding a single job and one for adding
three — first without a fixture, then refactored to use @pytest.fixture.
L11-4. Mount Point Validator: Write a function called
is_valid_mount(path, device) in mount_utils.py. The function should
return True if both path and device are non-empty strings and
path starts with /, and False otherwise. Write a test file
test_mount_utils.py that tests at least four cases.
L11-5. Server Fixture Test: Write a class called Server in
server_classes.py with attributes hostname and status (defaulting
to 'up'). Add methods shutdown() and start() that change the
status. Write a test file test_server_classes.py that uses a
@pytest.fixture to create a shared Server instance. Write at least
two test functions verifying shutdown() and start() work correctly.
Spanish / DELE C2
E11-1. Vocabulary Formatter Function: Write a function called
format_vocab_entry(palabra, definicion, capitulo=None) in a module
called vocab_utils.py. The function should return a formatted string
like hacer: to do/make (Ch. 30) if a chapter is provided, or
hacer: to do/make if not. Write a test file test_vocab_utils.py
with at least two test functions: one that tests the call with and
without the chapter argument.
E11-2. DELE Level Checker: Write a function called
is_target_level(current, target) in dele_utils.py. The function
should return True if current equals target and False otherwise.
Write a test file test_dele_utils.py that tests at least three cases:
matching levels, mismatched levels, and an empty current level.
E11-3. Chapter Notes Class: Write a class called ChapterNotes in
donquijote_classes.py. The init() method should accept a
chapter_number attribute and initialize an empty self.notas list.
Add a method add_nota(nota) that appends the note to self.notas.
Write a test file test_donquijote_classes.py with two test functions —
one for adding a single note and one for adding three — first without a
fixture, then refactored to use @pytest.fixture.
E11-4. Study Session Validator: Write a function called
is_valid_session(topic, duration_minutes) in study_utils.py. The
function should return True if topic is a non-empty string and
duration_minutes is a positive integer, and False otherwise. Write
a test file test_study_utils.py that tests at least four cases.
E11-5. Learner Fixture Test: Write a class called DELECandidate in
dele_candidate.py with attributes name and nivel and a sessions
list initialized to empty. Add a method log_session(topic) that
appends the topic to sessions. Write a test file
test_dele_candidate.py that uses a @pytest.fixture to create a
shared DELECandidate instance. Write at least two test functions
verifying that single and multiple sessions are logged correctly.