Skip to content

Commit 13dac11

Browse files
committed
Test suite for flask
1 parent 6dec95d commit 13dac11

File tree

2 files changed

+159
-0
lines changed

2 files changed

+159
-0
lines changed

tests/helpers/constants.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Constants shared by multiple tests"""
2+
3+
STANDARD_MSG_ATTRIBUTES = {
4+
"written_at",
5+
"written_ts",
6+
"msg",
7+
"type",
8+
"logger",
9+
"thread",
10+
"level",
11+
"module",
12+
"line_no",
13+
"correlation_id",
14+
}

tests/test_flask.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""Test suite for the flask backend"""
2+
import json
3+
import logging
4+
import pathlib
5+
import re
6+
import sys
7+
8+
import flask
9+
import pytest
10+
11+
from helpers import constants
12+
from helpers.handler import FormattedMessageCollectorHandler
13+
14+
LOGGER_NAME = "flask-test"
15+
16+
17+
@pytest.fixture
18+
def client_and_log_handler():
19+
import json_logging
20+
21+
# Init app
22+
app = flask.Flask(__name__)
23+
24+
# Init std logging
25+
logger = logging.getLogger(LOGGER_NAME)
26+
logger.setLevel(logging.DEBUG)
27+
handler = FormattedMessageCollectorHandler()
28+
logger.addHandler(handler)
29+
30+
# Add json_logging
31+
json_logging.init_flask(enable_json=True)
32+
json_logging.init_request_instrument(app)
33+
34+
# Prepare test endpoints
35+
@app.route("/log/levels/debug")
36+
def log_debug():
37+
logger.debug("debug message")
38+
return {}
39+
40+
@app.route("/log/levels/info")
41+
def log_info():
42+
logger.info("info message")
43+
return {}
44+
45+
@app.route("/log/levels/error")
46+
def log_error():
47+
logger.error("error message")
48+
return {}
49+
50+
@app.route("/log/extra_property")
51+
def extra_property():
52+
logger.info(
53+
"test log statement with extra props",
54+
extra={"props": {"extra_property": "extra_value"}},
55+
)
56+
return {}
57+
58+
@app.route("/log/exception")
59+
def log_exception():
60+
try:
61+
raise RuntimeError()
62+
except BaseException as e:
63+
logger.exception("Error occurred", exc_info=e)
64+
return {}
65+
66+
with app.test_client() as test_client:
67+
yield test_client, handler
68+
69+
# Tear down test environment
70+
logger.removeHandler(handler)
71+
del sys.modules["json_logging"] # "de-import" because json_logging maintains global state
72+
73+
74+
@pytest.mark.parametrize("level", ["debug", "info", "error"])
75+
def test_record_format_per_log_level(client_and_log_handler, level):
76+
api_client, handler = client_and_log_handler
77+
78+
response = api_client.get("/log/levels/" + level)
79+
80+
assert response.status_code == 200
81+
assert len(handler.messages) == 1
82+
msg = json.loads(handler.messages[0])
83+
assert set(msg.keys()) == constants.STANDARD_MSG_ATTRIBUTES
84+
assert msg["module"] == __name__
85+
assert msg["level"] == level.upper()
86+
assert msg["logger"] == LOGGER_NAME
87+
assert msg["type"] == "log"
88+
assert msg["msg"] == level + " message"
89+
assert re.match(
90+
r"^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(\.\d+.*)?$", msg["written_at"]
91+
), "The field 'written_at' does not contain an iso timestamp"
92+
93+
94+
def test_correlation_id_given(client_and_log_handler):
95+
api_client, handler = client_and_log_handler
96+
97+
response = api_client.get("/log/levels/debug", headers={"X-Correlation-Id": "abc-def"})
98+
99+
assert response.status_code == 200
100+
assert len(handler.messages) == 1
101+
msg = json.loads(handler.messages[0])
102+
assert set(msg.keys()) == constants.STANDARD_MSG_ATTRIBUTES
103+
assert msg["correlation_id"] == "abc-def"
104+
105+
106+
def test_correlation_id_generated(client_and_log_handler):
107+
api_client, handler = client_and_log_handler
108+
109+
response = api_client.get("/log/levels/debug")
110+
111+
assert response.status_code == 200
112+
assert len(handler.messages) == 1
113+
msg = json.loads(handler.messages[0])
114+
assert set(msg.keys()) == constants.STANDARD_MSG_ATTRIBUTES
115+
assert re.match(
116+
r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$",
117+
msg["correlation_id"],
118+
), "autogenerated UUID doesn't have expected format"
119+
120+
121+
def test_extra_property(client_and_log_handler):
122+
api_client, handler = client_and_log_handler
123+
124+
response = api_client.get("/log/extra_property")
125+
126+
assert response.status_code == 200
127+
assert len(handler.messages) == 1
128+
msg = json.loads(handler.messages[0])
129+
assert set(msg.keys()) == constants.STANDARD_MSG_ATTRIBUTES.union({"extra_property"})
130+
assert msg["extra_property"] == "extra_value"
131+
132+
133+
def test_exception_logged_with_stack_trace(client_and_log_handler):
134+
api_client, handler = client_and_log_handler
135+
136+
response = api_client.get("/log/exception")
137+
138+
assert response.status_code == 200
139+
assert len(handler.messages) == 1
140+
msg = json.loads(handler.messages[0])
141+
assert set(msg.keys()) == constants.STANDARD_MSG_ATTRIBUTES.union({"exc_info", "filename"})
142+
assert msg["filename"] == pathlib.Path(__file__).name, "File name for exception not logged"
143+
assert "Traceback (most recent call last):" in msg["exc_info"], "Not a stack trace"
144+
assert "RuntimeError" in msg["exc_info"], "Exception type not logged"
145+
assert len(msg["exc_info"].split("\n")) > 2, "Stacktrace doesn't have multiple lines"

0 commit comments

Comments
 (0)