K9s - Manage Your Kubernetes Clusters In Stylehttps://k9scli.io/
Kubernetes CLI To Manage Your Clusters In Style!
Kubernetes CLI To Manage Your Clusters In Style!
A comprehensive, hands-on and free course for building production-ready Kubernetes operators using Kubebuilder.
Argo CD is a declarative, GitOps continuous delivery tool for Kubernetes.
Find the documentation for all Kubernetes resources, properties, types, and examples.
Is It Worth Automating?
Sometimes it costs you more time to automate something than it does to just do it.
Hands-On Linux & DevOps
Real Challenges. Real Infra. Real Skills.
Master Linux & DevOps troubleshooting on live servers.
Fun, real-world challenges for engineers
and powerful assessments for hiring teams.
Get an existing Linux host into Ansible in seconds.
Enroll inspects a Debian-like or RedHat-like system, harvests the state that matters, and generates Ansible roles/playbooks so you can bring snowflakes under management fast.
Light, fluffy, and always free - The AWS Local Emulator alternative
To maintain a clean and organized codebase, it is standard practice to separate your application code from your test code. This is typically achieved by creating a dedicated tests directory at the project's root level.
devops_utils) and the tests directory as siblings.tests directory houses all the test files. It is common for the structure inside tests to mirror the structure of the application package to keep tests organized as the project grows.__init__.py file inside the tests directory to ensure it can be treated as a package if needed, although pytest's discovery mechanism is powerful enough to work without it in most cases.To test your application code, your test files must import the functions, classes, and variables that need to be verified. The standard and most robust way to do this is by using absolute imports that start from the project root.
tests/test_file_ops.py, will use an import statement like from devops_utils.file_ops import check_file_extension.sys.path).ModuleNotFoundError, because the project root is not automatically added to the path in that context. This is why a test runner like pytest is necessary.pytest for Project DiscoveryThe pytest framework is designed to handle the complexities of testing structured projects. When you run pytest from your project's root directory, it intelligently prepares the environment for test execution.
pyproject.toml or pytest.ini), which it uses to identify the project's root.pytest automatically adds this directory to sys.path.python -m pytestWhile running pytest directly is often sufficient, an even more reliable method is to invoke it as a module using the python -m flag. This approach is highly recommended, especially in automated environments like CI/CD pipelines.
python -m pytest guarantees that the current working directory is added to sys.path before pytest begins its execution.When setting up tests for a multi-file project, there are several common issues that can be easily avoided.
ModuleNotFoundError during test runs: This is the most frequent problem and is almost always caused by running pytest from the wrong directory. To avoid this, always run pytest from the project root, or preferably use python -m pytest. An editable install (pip install -e .) provides the most robust solution.conftest.py to define shared fixtures.from ..devops_utils import ... are fragile and should be avoided in test files. Always use absolute imports from the project root (e.g., from devops_utils.file_utils import ...).pyproject.tomlThe Python interpreter doesn't automatically know about our project's structure. The modern and most robust solution is to formally define our project as an installable package. By creating a standard pyproject.toml metadata file, we can perform an "editable install" using the command pip install -e .. This seamlessly links our source code into the virtual environment, making our packages importable from anywhere without manual path hacks or special commands. This is the standard, professional workflow for developing Python applications.
pyproject.toml FileThe pyproject.toml file is the modern, unified standard for configuring Python projects, replacing older files like setup.py.
setuptools, in the [build-system] table.[project] table.pip install -e .An "editable" or "development" install is a special mode for installing packages with pip.
pip install -e . installs the project from the current directory in "editable" mode.site-packages directory back to the original source code..py files are immediately reflected in the installed package without needing to run pip install again.Performing an editable install provides a definitive solution to the path and import issues encountered during development.
sys.path for the entire activated virtual environment.PYTHONPATH environment variable.python -mWhile a script with an if __name__ == "__main__" block can be run with python -m, defining a console script in pyproject.toml is the preferred professional approach.
ping-check) is short and intuitive. A python -m command is long, exposes the internal module path, and is easy to mistype.pyproject.toml file, the command remains the same for the user. The python -m command breaks if you rename or move the target file.pyproject.toml clearly marks it as a public, supported command-line interface for your package.[project]
name = "devops_utils"
version = "0.1.0"
[project.scripts]
check_host = "devops_utils.network_utils.check_host:main"python -m vs. python file.pyThe key to understanding the difference lies in the first entry of sys.path. When Python initializes, it needs to know where to start looking for modules. The way you call it determines this "entry point zero".
When you run a script directly using python path/to/script.py, the interpreter's main task is to execute that specific file. It sets the first entry of sys.path to be the directory that contains the script.
When you run a script as a module using python -m package.module, the interpreter's goal is to locate and run a module within an importable package. It sets the first entry of sys.path to be the current working directory from which the command was executed. This allows absolute imports from the project root to succeed.
While you can run any file with python -m, it can lead to a RuntimeWarning if the file is both a library (meant to be imported) and a script (meant to be run). The best practice is to separate these roles.
file_ops.py and network_ops.py) should contain only reusable functions and classes. They should not contain an if __name__ == "__main__": block for complex script logic.python file.py will often cause ModuleNotFoundError for absolute imports. Avoid this by always running packaged scripts from the project root using python -m.RuntimeWarning. Avoid this by separating concerns: create dedicated runner scripts that import from your library modules.-m. The command must be the full dotted path to the script from the project root (e.g., python -m package.subpackage.script).__init__.py)A Python package provides a way to structure a project's module namespace by using directories. It enables a hierarchical organization of modules, mirroring the file system's structure.
__init__.py is recognized by the Python interpreter as a package..py) but also other subdirectories that are themselves packages.__init__.py__init__.py is the standard and most compatible method.__init__.py file is executed once when the package or any of its modules are first imported, which can be useful for setting up package-level resources. For many packages, this file can simply be left empty.import devops_utils.file_ops. Accessing its contents would then require the full prefix, like devops_utils.file_ops.check_file_extension().from keyword to import the module more directly, as in from devops_utils import file_ops, which then allows access via file_ops.check_file_extension().from devops_utils.file_ops import check_file_extension. This makes the function available to be called directly as check_file_extension().__init__.py to Control Imports__init__.py file itself, you can make them appear as if they belong to the top-level package namespace.To improve organization, we can group related modules into their own dedicated subpackages. A subpackage is a directory inside a parent package that contains its own __init__.py file.
devops_utils, can contain multiple subpackages.file_utils subpackage for file-related modules and a network_utils subpackage for networking modules.__init__.py file (which can be empty) to be recognized by Python as a package.An absolute import provides the complete, explicit path to a module starting from a top-level directory that is on Python's search path (sys.path).
from package.subpackage.module import function.devops_utils package would use from devops_utils.file_utils.file_ops import check_file_extension to access a function.A relative import specifies the path to a module based on the location of the file performing the import. This is done using dot notation.
. refers to the current package, while .. refers to the parent package.from .sibling_module import name brings in a name from a module in the same directory. An import like from ..parent_sibling.module import name navigates one level up and then down into a sibling package.A significant pitfall arises when you attempt to directly execute a Python file that contains relative imports. This action will almost always result in an error.
python devops_utils/network_utils/network_ops.py will raise an ImportError: attempted relative import with no known parent package.__name__) to "__main__" and does not recognize it as being part of a package. Consequently, it cannot resolve relative paths like . or ...import System.py extension can be treated as a module..py suffix. For example, a file named file_ops.py is imported as the file_ops module.import Statementimport module_namemodule_name.function_name.CONFIG in your script will not conflict with module_name.CONFIG.from ... import ....as keyword to rename an imported object, for example, from file_ops import parse_yaml_file as parse_yaml.parse_yaml() instead of file_ops.parse_yaml_file()).my_function and later define your own function with the same name, the original import will be overwritten.from module import * is strongly discouraged because it imports all public names from the module, which can pollute the local namespace and make the code difficult to read and debug.When you execute an import statement, Python needs to locate the corresponding module file. It does this by searching through a specific list of directories.
sys.path, which is part of the standard sys module.sys.path list is automatically populated and typically includes the directory of the script that is currently running, directories specified in the PYTHONPATH environment variable, and the default locations where Python and third-party packages are installed.main.py can seamlessly import utils.py when both files are located in the same folder.Example of main.py
print("Main script starting...")
from devops_utils import (
check_file_extension,
is_host_up,
check_hosts_from_config,
)
import sys
print(sys.path)
filenames = ["config.yaml", "script.sh"]
for filename in filenames:
print(f"Checking {filename}")
print(f"Result: {check_file_extension(filename)}")
print(f"\nIs localhost up? {is_host_up("localhost")}")
print(
f"Is nonexistenthost12345 up? {is_host_up("nonexistenthost12345")}"
)
print(
f"\nAre all hosts from servers_config.yaml up? {check_hosts_from_config("servers_config.yaml")}"
)
Example of file_ops.py
print("Module file_ops is being imported")
from typing import Any
try:
import yaml
except (ModuleNotFoundError, ImportError):
print(
"Warning: PyYAML not found, parse_yaml_file will not work."
)
yaml = None
SUPPORTED_EXTENSIONS: list[str] = [".json", ".yaml", ".txt"]
def check_file_extension(filename: str) -> bool:
"""Checks if a file has a supported extension"""
print(
f" - file_ops.check_file_extension called for {filename}"
)
return any(
filename.endswith(ext) for ext in SUPPORTED_EXTENSIONS
)
def parse_yaml_file(path_str: str) -> dict[str, Any]:
"""Parses a YAML file and returns its contents."""
print(f" - file_ops.parse_yaml_file called for {path_str}")
if yaml:
with open(path_str, "r") as file:
return yaml.safe_load(file)
else:
return {}
unittest.mock module provides tools to create and configure these mock objects and to track interactions.patch function replaces a target object with a mock in a specified scope, either for the duration of a function (decorator) or within a context block (with).patch injects the mock into the test function’s parameters; as a context manager, it yields the mock within the with block.MagicMock instance that you can configure.mock.return_value to define what the mock will return when called.mock.side_effect to simulate an exception being raised by the mock when invoked, to pass different values to be returned by each execution, or to pass a calable to replace the implemented function.assert_called_with and assert_called_once let you verify interactions with the mock.requests.get or requests.post to simulate successful responses, HTTP errors, or timeouts.open() or os.path.exists() to simulate file presence or content.subprocess.run to avoid running real system commands and control return codes.time.sleep or mock datetime.now() to remove delays and make time-based tests deterministic.from unittest.mock import patch, Mock
from pytest_mock import MockerFixture
from dummy_functions import check_file_exists, get_user_data
# Section: Using unittest.mock.patch
def test_check_file_exists_manual_patch() -> None:
filepath = "/path/to/some/file.txt"
patcher = patch("dummy_functions.os.path.exists")
mock_exists = patcher.start()
mock_exists.return_value = True
try:
result = check_file_exists(filepath=filepath)
mock_exists.assert_called_once_with(filepath)
assert result is True
finally:
patcher.stop()
def test_check_file_exists_context_manager() -> None:
filepath = "/path/to/some/file.txt"
with patch("dummy_functions.os.path.exists") as mock_exists:
mock_exists.return_value = True
result = check_file_exists(filepath=filepath)
mock_exists.assert_called_once_with(filepath)
assert result is True
@patch("dummy_functions.os.path.exists")
def test_check_file_exists_decorator(mock_exists: Mock) -> None:
filepath = "/path/to/some/file.txt"
mock_exists.return_value = True
result = check_file_exists(filepath=filepath)
mock_exists.assert_called_once_with(filepath)
assert result is True
def test_check_file_pytest_mocker(mocker: MockerFixture) -> None:
filepath = "/path/to/some/file.txt"
mock_exists = mocker.patch("dummy_functions.os.path.exists")
mock_exists.return_value = True
result = check_file_exists(filepath=filepath)
mock_exists.assert_called_once_with(filepath)
assert result is True
# Section: MagicMock and Configuring Mock Objects
def test_get_user_data_success(mocker: MockerFixture) -> None:
mock_api_response: dict[str, str | int] = {
"id": 1,
"name": "test user",
}
mock_get = mocker.patch("dummy_functions.requests.get")
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = mock_api_response
data = get_user_data(user_id=1)
mock_get.assert_called_once_with(
"https://api.example.com/users/1"
)
assert data == mock_api_response
Example of dummy_function.py
import requests
import os
import subprocess
from typing import Optional, Any
def get_user_data(user_id: str | int) -> dict[str, str | int]:
response = requests.get(
f"https://api.example.com/users/{user_id}"
)
print(f"Status code: {response.status_code}")
response.raise_for_status()
return response.json()
def check_file_exists(filepath: str | os.PathLike[str]) -> bool:
return os.path.exists(filepath)
def get_external_ip():
"""Fetches the current external IP from an external service."""
try:
response = requests.get(
"https://api.ipify.org?format=json", timeout=5
)
response.raise_for_status()
return response.json().get("ip")
except requests.exceptions.RequestException:
return None
def get_current_user() -> Optional[str]:
try:
result = subprocess.run(
["whoami"],
capture_output=True,
text=True,
check=True,
timeout=5,
)
return result.stdout.strip()
except (
subprocess.CalledProcessError,
subprocess.TimeoutExpired,
FileNotFoundError,
):
return None
def fetch_both_endpoints() -> (
tuple[dict[str, Any], dict[str, Any]]
):
"""
Fetch data from two endpoints and return their JSON responses as a tuple.
"""
response2 = requests.get("https://api.example.com/second")
response2.raise_for_status()
data2 = response2.json()
response1 = requests.get("https://api.example.com/first")
response1.raise_for_status()
data1 = response1.json()
return data1, data2
side_effectside_effect attribute on a mock allows you to control its behavior beyond a single return value.side_effect is set to a list, each call to the mock returns the next item in that list, in order.side_effect is a function, it is called with the same arguments as the mock, and its return value is used as the mock’s return.side_effect is an exception, it will raise that exception when the original function is called.Mock: A simple replacement that only creates attributes when accessed, and raises errors for undefined methods or attributes.MagicMock: Inherits from Mock and implements Python’s magic methods (__len__, __enter__, etc.) by default.Mock by default for stubbing external dependencies to catch unintended interactions.MagicMock only when mocking objects that require special behavior, such as context managers or iterables.import subprocess
import pytest
from unittest.mock import MagicMock
from pytest_mock import MockerFixture
from dummy_functions import (
get_current_user,
check_file_exists,
fetch_both_endpoints,
)
# Section: Using side_effect - Exceptions
def test_get_current_user_command_fails(mocker: MockerFixture):
mock_run = mocker.patch("dummy_functions.subprocess.run")
mock_run.side_effect = subprocess.CalledProcessError(
returncode=1, cmd=["whoami"]
)
result = get_current_user()
assert result is None
# Section: Using side_effect - List for Multiple Calls
def test_check_file_exists_side_effect_list(
mocker: MockerFixture,
):
mock_exists = mocker.patch(
"dummy_functions.os.path.exists",
side_effect=[True, False],
)
assert check_file_exists("some/path/one") is True
assert check_file_exists("some/path/two") is False
assert mock_exists.call_count == 2
assert [
call.args for call in mock_exists.call_args_list
] == [("some/path/one",), ("some/path/two",)]
# Section: Using side_effect - Callable for Multiple Calls
def test_fetch_both_endpoints_by_url(mocker: MockerFixture):
fake_responses: dict[str, MagicMock] = {}
for url, data in [
("https://api.example.com/first", {"first": "data"}),
("https://api.example.com/second", {"second": "data"}),
]:
resp = mocker.MagicMock()
resp.status_code = 200
resp.json.return_value = data
fake_responses[url] = resp
def _fake_get(url: str) -> MagicMock:
return fake_responses[url]
mocker.patch(
"dummy_functions.requests.get", side_effect=_fake_get
)
result = fetch_both_endpoints()
assert result == ({"first": "data"}, {"second": "data"})
# Section: Choosing between Mock and MagicMock
@pytest.mark.xfail(
reason="Context managers do not work with Mock", strict=True
)
def test_context_manager_with_mock(mocker: MockerFixture):
fake_cm = mocker.Mock()
fake_cm.__enter__.return_value = fake_cm
fake_cm.read.return_value = "file contents"
mock_open = mocker.patch("builtins.open")
mock_open.return_value = fake_cm
with open("somefile.txt") as f:
contents = f.read()
mock_open.assert_called_once_with("somefile.txt")
assert contents == "file contents"
def test_context_manager_with_magicmock(mocker: MockerFixture):
fake_cm = mocker.MagicMock()
fake_cm.__enter__.return_value = fake_cm
fake_cm.read.return_value = "file contents"
mock_open = mocker.patch("builtins.open")
mock_open.return_value = fake_cm
with open("somefile.txt") as f:
contents = f.read()
mock_open.assert_called_once_with("somefile.txt")
assert contents == "file contents"
@pytest.mark.parametrize(argnames, argvalues) decorator takes argument names and a list of value tuples to generate multiple test invocations.argvalues list corresponds to a separate run of the test function, with tuple elements mapped to argument names.-v shows each parametrized case as a distinct test, simplifying result interpretation.pytest.param Constructpytest.param() function wraps a set of parameter values and allows you to attach metadata to that invocation:
id: a custom label shown in the test report.marks: one or more markers (e.g., pytest.mark.xfail, pytest.mark.skip) applied only to that case.pytest.param(..., id="custom_id") to assign clear, human-readable names to individual cases.ids list passed to parametrize can specify identifiers in the order of argvalues.import pytest
def is_valid_hostname_char(char: str) -> bool:
if "a" <= char <= "z":
return True
if "0" <= char <= "9":
return True
if char == "-":
return True
return False
def check_url_status(url: str) -> tuple[int | str, str]:
if url == "https://google.com":
return (200, "OK")
if url == "https://fakesite123.org/notfound":
return (404, "HTTP_ERROR (404)")
if url == "http://httpbin.org/status/503":
return (503, "HTTP_ERROR (503)")
if url == "http://localhost:1":
return ("CONNECTION_ERROR", "CONNECTION_ERROR")
return ("UNKNOWN", "UNKNOWN")
# Section: The Problem: Duplicated Test Logic
"""
a -> True
5 -> True
- -> True
A -> False
_ -> False
"""
def test_is_valid_lower_case_a():
assert is_valid_hostname_char("a") is True
def test_is_valid_5():
assert is_valid_hostname_char("5") is True
def test_is_valid_hyphen():
assert is_valid_hostname_char("-") is True
def test_is_valid_upper_case_A():
assert is_valid_hostname_char("A") is False
def test_is_valid_underscore():
assert is_valid_hostname_char("_") is False
# Section: Solution: @pytest.mark.parametrize
@pytest.mark.parametrize(
"input_char, expected_result",
[
("a", True),
("5", True),
("-", True),
("A", False),
("_", False),
("!", False),
],
)
def test_is_valid_hostname_char(
input_char: str, expected_result: bool
):
assert is_valid_hostname_char(input_char) is expected_result
# Section: Customizing Test IDs with pytest.param construct
@pytest.mark.parametrize(
"input_char, expected_result",
[
pytest.param("a", True, id="lowercase_letter_a"),
pytest.param("z", True, id="lowercase_letter_z"),
pytest.param("0", True, id="digit_0"),
pytest.param("-", True, id="hyphen"),
pytest.param("A", False, id="uppercase_A_invalid"),
pytest.param("_", False, id="underscore_invalid"),
],
)
def test_is_valid_hostname_custom_params(
input_char: str, expected_result: bool
):
assert is_valid_hostname_char(input_char) is expected_result
@pytest.mark.parametrize(
"url_to_check, expected_status_code, expected_status_text",
[
("https://google.com", 200, "OK"),
(
"https://fakesite123.org/notfound",
404,
"HTTP_ERROR (404)",
),
(
"http://httpbin.org/status/503",
503,
"HTTP_ERROR (503)",
),
(
"http://localhost:1",
"CONNECTION_ERROR",
"CONNECTION_ERROR",
),
pytest.param(
"https://pending.retries.tests",
503,
"HTTP_ERROR (503)",
marks=(
pytest.mark.xfail(
reason="Retry logic for 503 is not yet implemented."
),
pytest.mark.api,
),
),
],
ids=[
"google_ok",
"site_not_found",
"server_error_503",
"connection_error",
"xfail_retry_case",
],
)
def test_various_url_statuses(
url_to_check: str,
expected_status_code: int,
expected_status_text: str,
):
status_code, status_text = check_url_status(url_to_check)
assert status_code == expected_status_code
assert status_text == expected_status_text
@pytest.fixture that provides a baseline environment or data for tests.@pytest.fixture@pytest.fixture above a function to mark it as a fixture that can return or yield resources.yield is setup, the yielded value goes to the test, and code after yield is teardown.function (default), class, module, or session.function-scoped fixture runs for each test function; a session-scoped fixture runs only once for the entire test session.yield rather than return.yield always runs, even if the test fails or raises an error.try...finally block, ensuring resources like temporary files or connections are properly released.conftest.py: Sharing Fixturesconftest.py file makes them available across multiple test modules without explicit imports.conftest.py in a test directory or its parent enables automatic discovery of shared fixtures.import pytest
from typing import Iterable
ManagedResource = dict[str, str]
@pytest.fixture
def managed_resource() -> Iterable[ManagedResource]:
print(" [SHARED FIXTURE]: acquiring resource lock")
resource = {"status": "lock_acquired"}
yield resource
print(" [SHARED FIXTURE]: releasing resource lock")
resource["status"] = "lock_released"@pytest.mark.<markername>) applied to tests to attach metadata.skip, skipif, xfail, and parametrize provide predefined behaviors.slow, integration, api) help categorize tests by project-specific criteria.@pytest.mark.skipskip marker unconditionally prevents a test from running, useful when a feature is disabled or under refactor.s in the summary along with the provided reason.@pytest.mark.skip(reason="...") to disable tests without removing their code.@pytest.mark.skipifskipif marker skips tests only when a specified condition evaluates to true at collection time.sys.platform != "linux" or checks for optional dependencies.@pytest.mark.xfailxfail marker marks tests expected to fail due to known bugs or unimplemented features.XFAIL and don’t cause the suite to fail; unexpected passes are reported as XPASS.slow, api, integration) help organize and categorize tests by functionality or runtime.@pytest.mark.api and @pytest.mark.integration).pytest.ini or pyproject.toml under the markers option.-m <expression> CLI option selects tests matching a marker expression.and, or, and negation with not.pytest -m slow, pytest -m "not slow", pytest -m "api and integration".skip hides failures; prefer xfail for known bugs to maintain visibility.skipif conditions reduce readability; delegate logic to helper functions when needed.import pytest
import time
try:
import some_optional_library # type: ignore
except ModuleNotFoundError:
some_optional_library = None
# Section: Skipping Tests Unconditionally: @pytest.mark.skip
@pytest.mark.skip(
reason="Skipping experimental feature until completion."
)
def test_new_experimental_feature() -> None:
assert False
# Section: Skipping Tests Conditionally: @pytest.mark.skipif
@pytest.mark.skipif(
some_optional_library is None,
reason="Requires 'some_optional_library' to be installed.",
)
def test_with_optional_dependency() -> None:
print(
f"Running tests that depends on an optional library..."
)
assert some_optional_library
# Section: Expected Failures: @pytest.mark.xfail
@pytest.mark.xfail(
reason="Bug #123: Division by zero not handled properly."
)
def test_divide_by_zero() -> None:
_division = 1 / 0
assert False
@pytest.mark.xfail # Add strict=True to make XPASS lead to a failure
def test_expected_to_fail() -> None:
assert True
# Section: Custom Markers and Registration
@pytest.mark.slow
def test_very_long_computations() -> None:
time.sleep(5)
assert True
@pytest.mark.api
@pytest.mark.smoke
def test_user_creation() -> None:
assert True
# Section: Running Tests by Marker (m option)