From b04814eda34b1827825a106e55c0ad0a4a7eb9e9 Mon Sep 17 00:00:00 2001 From: Namah Shrestha Date: Sun, 23 Oct 2022 00:52:47 +0530 Subject: [PATCH 1/3] (test) Added tests for checking configuration, checking results. Added fixtures on conftest. --- setup.cfg | 1 + tests/__init__.py | 0 tests/conftest.py | 49 ++++++++++- tests/test_logger.py | 190 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 236 insertions(+), 4 deletions(-) create mode 100644 tests/__init__.py diff --git a/setup.cfg b/setup.cfg index c33a165..70e413c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ testing = setuptools pytest pytest-cov + testfixtures [tool:pytest] addopts = diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 4a633aa..f34f57e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,53 @@ Read more about conftest.py under: - https://docs.pytest.org/en/stable/fixture.html - https://docs.pytest.org/en/stable/writing_plugins.html + + By convention storing fixutres in this file allows the fixture + to be accessed by all the modules within the test package. """ -# import pytest +import io +import logging +import sys + +import pytest + +import safe_security_logger as logger + + +@pytest.fixture(scope="class") +def get_safe_security_logger(request) -> logger: + request.cls.logger: logging.Logger = logger.getLogger("test-safe-logger") + + +@pytest.fixture(scope="function") +def capture_stdout_output(monkeypatch) -> dict: + """ + Returns a logoutput dictionary. + Mocks the standard ouput write and captures data + from logger. + + Uses monkeypatch fixture to do mocking. + """ + logoutput: dict = {"record": [], "write_count": 0} + + def fake_write(log_data: str) -> dict: + logoutput["record"].append(log_data) + logoutput["write_count"] += 1 + return logoutput + + monkeypatch.setattr(sys.stdout, "write", fake_write) + return logoutput + + +@pytest.fixture(scope="function") +def capture_open_output(monkeypatch) -> dict: + logoutput: dict = {"record": [], "write_count": 0} + + def fake_write(log_data: str) -> dict: + logoutput["record"].append(log_data) + logoutput["write_count"] += 1 + return logoutput + + monkeypatch.setattr(io, "open", fake_write) + return logoutput diff --git a/tests/test_logger.py b/tests/test_logger.py index e829c17..1557c59 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,3 +1,12 @@ +# built-ins +import logging +import sys +import typing + +# third party +import pytest + +# packages import safe_security_logger as logger __author__ = "deepak.s@safe.security" @@ -5,6 +14,181 @@ __license__ = "MIT" -def test_logger(): - safelogger = logger.getLogger("safe-logger") - assert safelogger.name == "safe-logger" +class TestGetLoggerConfigurations: + """ + This test class will test the getLogger function inside + src.safe_security_logger.logger.py + + getLogger creates the logger. + + In this test class, + - We need to make sure that all the configurations are properly set. + - This test class lets us be sure that the problems related to + improper configurations are tested. + + We test: + - handler settings + - log level settings + - formatter settings + + Author: Namah Shrestha + """ + + @pytest.mark.parametrize( + "loglevel, expected_loglevel", + [ + (logging.NOTSET, 0), + (logging.DEBUG, 10), + (None, 20), + (logging.WARN, 30), + (logging.ERROR, 40), + (logging.CRITICAL, 50), + ], + ) + def test_log_level( + self, loglevel: typing.Union[int, None], expected_loglevel: int + ) -> None: + """ + Theory: + ------- + Python has six log levels. + Each one is assigned a specific integer, + indicating the severity of the log. + + NOTSET=0 + DEBUG=10 + INFO=20 + WARN=30 + ERROR=40 + CRITICAL=50 + + Tests: + ------ + 1. getLogger says that the default level of log should be INFO + if no level is provided. + 2. All other logging levels should be set by the level parameter. + + Author: Namah Shrestha + """ + logname: str = "test-safe-logger" + if loglevel is not None: + logr: logging.Logger = logger.getLogger(logname, level=loglevel) + else: + logr: logging.Logger = logger.getLogger(logname) + assert logr.level == expected_loglevel + + @pytest.mark.parametrize( + "handlers_input, handler_names", + [ + ([], {"StreamHandler"}), + ([logging.FileHandler("dummy_file.log")], {"StreamHandler", "FileHandler"}), + ], + ) + def test_handlers(self, handlers_input: list, handler_names: set[str]) -> None: + """ + Theory: + ------- + There are three core handlers in the logging module. + 1. StreamHandler: + - Sends logging output to streams. + - Such as, sys.stdout, sys.stderr, or any file-like object. + - Precisely, any object which supports write() and flush() methods. + 2. FileHandler: + - Sends logging output to a disk file. + - Inherits the output functionality from StreamHandler. + 3. NullHandler: + - It does not do any formatting or output. + - It is essentially a `no-op` handler for use by library developers. + All 3 classes are located in the core logging package. + + There are other handlers located at logging.handlers package + built for their own purposes. + Read more: https://docs.python.org/3/library/logging.handlers.html + + Tests: + ------ + 1. console_handler is a StreamHandler sending output to sys.stdout. + It should be added by default. + 2. We should be able to add other handlers using the handlers list. + + Author: Namah Shrestha + """ + get_handler_names: callable = lambda x: { + str(item).split(" ")[0][1:] for item in x.handlers + } + logr: logging.Logger = logger.getLogger( + "test-safe-logger", handlers=handlers_input + ) + assert get_handler_names(logr) == handler_names + + +@pytest.mark.usefixtures("get_safe_security_logger", "capture_stdout_output") +class TestGetLoggerResults: + """ + This test class will test the getLogger function inside + src.safe_security_logger.logger.py + + getLogger creates the logger. + + In this test class, + - We make sure the logger behaves the way we want it to. + - By testing the actual results. + - We will rely heavily on monkeypatching to capture output results. + + Author: Namah Shrestha + """ + + def test_setup_is_working(self) -> None: + """ + This test function is written to assure that the fixtures are working fine. + - The logger is created which means get_safe_security_logger + fixture is working. + - `fake_write` in `sys.stdout.write` means that, sys.stdout.write + has been successfully mocked. + - `fake_write` in `builtins.open` means that, + io.open has been succesfully mocked. + + Author: Namah Shrestha + """ + assert self.logger.name == "test-safe-logger" + assert "fake_write" in str(sys.stdout.write) + # uncomment if you have monkeypatched io.open + # assert 'fake_write' in str(io.open) + + @pytest.mark.parametrize( + "inputstr, extra, expected_output", + [ + ( + "Hello World", + {"text": "world"}, + { + "timestamp": "2022-10-24 06:18:11,710", + "level": "INFO", + "message": "Hello", + "type": "application", + "metadata": { + "loggerName": "awesome-logger", + "module": "", + "functionName": "", + "lineno": 1, + }, + }, + ) + ], + ) + def test_results_for_streaming_handler( + self, inputstr: typing.Any, extra: dict, expected_output: dict + ) -> None: + """ + This test will test the output of the streaming handler log. + With various input configurations. + + Current Issue: + - Unable to capture logs from sys.stdout. + - StreamHandler should be writing to sys.stdout. + - But monkeypatching sys.stdout is not working. + - If this happens many times, we might go with a simple + unittest method. + s + Author: Namah Shrestha + """ From 96dcd224ecd16a6e1af4408c20de4a41c083e071 Mon Sep 17 00:00:00 2001 From: Namah Shrestha Date: Sun, 30 Oct 2022 00:28:09 +0530 Subject: [PATCH 2/3] (test)Removed fixtures from conftest. Used unittest.mock.patch to capture stdout.write --- tests/conftest.py | 46 +----------------- tests/test_logger.py | 112 +++++++++++++++++++------------------------ 2 files changed, 49 insertions(+), 109 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f34f57e..c6bb053 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,48 +10,4 @@ to be accessed by all the modules within the test package. """ -import io -import logging -import sys - -import pytest - -import safe_security_logger as logger - - -@pytest.fixture(scope="class") -def get_safe_security_logger(request) -> logger: - request.cls.logger: logging.Logger = logger.getLogger("test-safe-logger") - - -@pytest.fixture(scope="function") -def capture_stdout_output(monkeypatch) -> dict: - """ - Returns a logoutput dictionary. - Mocks the standard ouput write and captures data - from logger. - - Uses monkeypatch fixture to do mocking. - """ - logoutput: dict = {"record": [], "write_count": 0} - - def fake_write(log_data: str) -> dict: - logoutput["record"].append(log_data) - logoutput["write_count"] += 1 - return logoutput - - monkeypatch.setattr(sys.stdout, "write", fake_write) - return logoutput - - -@pytest.fixture(scope="function") -def capture_open_output(monkeypatch) -> dict: - logoutput: dict = {"record": [], "write_count": 0} - - def fake_write(log_data: str) -> dict: - logoutput["record"].append(log_data) - logoutput["write_count"] += 1 - return logoutput - - monkeypatch.setattr(io, "open", fake_write) - return logoutput +# import pytest diff --git a/tests/test_logger.py b/tests/test_logger.py index 1557c59..8e0f04a 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,7 +1,9 @@ # built-ins +import json import logging -import sys import typing +import unittest +import unittest.mock as mock # third party import pytest @@ -122,73 +124,55 @@ def test_handlers(self, handlers_input: list, handler_names: set[str]) -> None: assert get_handler_names(logr) == handler_names -@pytest.mark.usefixtures("get_safe_security_logger", "capture_stdout_output") -class TestGetLoggerResults: +class TestGetLoggerResults(unittest.TestCase): """ - This test class will test the getLogger function inside - src.safe_security_logger.logger.py - - getLogger creates the logger. - - In this test class, - - We make sure the logger behaves the way we want it to. - - By testing the actual results. - - We will rely heavily on monkeypatching to capture output results. - - Author: Namah Shrestha + This test class uses unittest. + Just to mock the stdout.write. + And to assert that stdout.write + was called with the expected output. """ - def test_setup_is_working(self) -> None: - """ - This test function is written to assure that the fixtures are working fine. - - The logger is created which means get_safe_security_logger - fixture is working. - - `fake_write` in `sys.stdout.write` means that, sys.stdout.write - has been successfully mocked. - - `fake_write` in `builtins.open` means that, - io.open has been succesfully mocked. + def setUp(self) -> None: + self.streamlogr: logging.Logger = logger.getLogger("test-stream-logger") - Author: Namah Shrestha + def compare_while_ignoring( + self, first: dict, second: dict, ignore_keys: list = [] + ) -> bool: """ - assert self.logger.name == "test-safe-logger" - assert "fake_write" in str(sys.stdout.write) - # uncomment if you have monkeypatched io.open - # assert 'fake_write' in str(io.open) - - @pytest.mark.parametrize( - "inputstr, extra, expected_output", - [ - ( - "Hello World", - {"text": "world"}, - { - "timestamp": "2022-10-24 06:18:11,710", - "level": "INFO", - "message": "Hello", - "type": "application", - "metadata": { - "loggerName": "awesome-logger", - "module": "", - "functionName": "", - "lineno": 1, - }, - }, - ) - ], - ) - def test_results_for_streaming_handler( - self, inputstr: typing.Any, extra: dict, expected_output: dict - ) -> None: + Function compares each key of first and second dictionaries. + It will ignore keys that are specified in the ignore_keys list. + """ + for key, value in first.items(): + if key in ignore_keys: + continue + if key not in second: + return False + if value != second[key]: + return False + return True + + @mock.patch("sys.stdout.write") + def test_results_for_streaming_handler(self, fake_write) -> None: """ - This test will test the output of the streaming handler log. - With various input configurations. - - Current Issue: - - Unable to capture logs from sys.stdout. - - StreamHandler should be writing to sys.stdout. - - But monkeypatching sys.stdout is not working. - - If this happens many times, we might go with a simple - unittest method. - s - Author: Namah Shrestha + Here we test the actual result of the log + with the expected output. """ + input: str = "asd" + expected_output: dict = { + "timestamp": "2022-10-29 12:10:33,699", + "level": "INFO", + "message": "asd", + "type": "application", + "metadata": { + "loggerName": "test-stream-logger", + "module": "test_logger", + "functionName": "test_results_for_streaming_handler", + "lineno": 173, + }, + } + self.streamlogr.info(input) + actual_output: dict = json.loads(fake_write.call_args_list[0][0][0]) + result: bool = self.compare_while_ignoring( + expected_output, actual_output, ignore_keys=["timestamp"] + ) + self.assertEqual(result, True) From e89c449e239213e01a29f1ce7f84ec91a15c2048 Mon Sep 17 00:00:00 2001 From: Namah Shrestha Date: Wed, 2 Nov 2022 21:45:38 +0530 Subject: [PATCH 3/3] (test) Removed str from set in test_logger --- tests/test_logger.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_logger.py b/tests/test_logger.py index 8e0f04a..eb65b1d 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -86,7 +86,7 @@ def test_log_level( ([logging.FileHandler("dummy_file.log")], {"StreamHandler", "FileHandler"}), ], ) - def test_handlers(self, handlers_input: list, handler_names: set[str]) -> None: + def test_handlers(self, handlers_input: list, handler_names: set) -> None: """ Theory: -------