pytest 커버리지 확인하기

2024년 8월 12일 월요일

Today I Learned

날짜

2024년 8월 12일 월요일

내용

테스트코드 정상화

유지보수 중 생각보다 시간 여유가 생겨 테스트코드를 작성했다. 가장 자주 문제가 되는 부분인 서비스 활성화 부분에 대한 코드다. 특정 앱을 사용 시작했을 때 shop_detail이 제대로 바뀌는지 확인하는 코드다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
class TestServiceActivation:
    """
    각 서비스 활성화 테스트
    """

    @pytest.mark.parametrize(
        "alpha_app",
        [
            app
            for app in AlphaApps.__members__.values()
            if app != AlphaApps.BROWSE_BOOSTER
        ],
    )
    @patch("src.routers.shops", new_callable=AsyncMock)
    @patch("aioboto3.Session.resource", new_callable=AsyncMock)
    def test_patch_shop_detail(
        shop_detail, mock_s3_resource, mock_send_email_for_starting_user, alpha_app
    ):
        """
        Product Reviews, List Designer, Browse Booster를 활성화한 후
        shop_detail을 업데이트하여 정상적으로 활성화되었는지 확인
        """
        with TestingSessionWriteLocal() as db:
            result = False

            set_shop_factory_session(db)

            db_detail = ShopDetailFactory.create()
            db.add(db_detail)
            db.commit()

            apps_log = create_initial_shopify_apps()
            new_apps_log = json.loads(apps_log)
            new_apps_log[str(alpha_app.value)] = {
                "is_use": True,
                "created_at": datetime.now(tz=timezone.utc).strftime(
                    "%Y-%m-%dT%H:%M:%S.%f%z"
                ),
            }

            new_shop_detail = {
                "company_name": db_detail.company_name,
                "industry": db_detail.industry,
                "store_name": db_detail.store_name,
                "store_url": db_detail.store_url,
                "shop_owner": db_detail.shop_owner,
                "email": db_detail.email,
                "phone": db_detail.phone,
                "address": db_detail.address,
                "customer_email": db_detail.customer_email,
                "store_logo": db_detail.store_logo,
                "is_dropshipper": db_detail.is_dropshipper,
                "apps_log": json.dumps(new_apps_log),
                "use_apps": [alpha_app.value],
            }
            response = client.patch(
                url="/shops/detail",
                data={
                    "shop_detail": json.dumps(new_shop_detail),
                },
                params={"shop_access_code": db_detail.shop.access_code},
            )

            if response.status_code == 200:
                response = response.json()
                arr = get_use_apps(apps_log=response["apps_log"])
                if alpha_app in arr:
                    result = True

            db.delete(db_detail.shop)
            db.delete(db_detail)
            db.commit()
            assert result

    @classmethod
    def teardown_class(cls):
        with TestingSessionWriteLocal() as db:
            db.query(models.Shop).delete()
            db.commit()

mock_s3_resourcemock_send_email_for_starting_user 는 쓰이지 않는데, 뺴면 아예 오류가 뜬다. 원인이 뭐꼬…

처음에 제대로 작동이 안됐는데 이전 테스트에서 생성된 shop이 데이터베이스에 남아있어서 문제가 됐다. 각 유닛테스트가 끝나면 꼭 데이터베이스를 초기화 하는게 중요하다.

테스트코드의 커버리지

테스트 코드가 실제 코드들을 얼마나 실행하는지를 나타내는 의미다. 100%이면 테스트 코드를 통해 모든 코드를 테스트하고 있다는 의미다.

coverage run -m pytest 로 테스트한 후, coverage report -m 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#Pytest 실행
root@c5a7e41c3e84:/code# coverage run -m pytest

# Pytest 결과
============================== test session starts ==============================
platform linux -- Python 3.10.14, pytest-7.2.0, pluggy-1.0.0
rootdir: /code
plugins: Faker-18.5.1, cov-5.0.0, anyio-3.6.2, asyncio-0.21.0
asyncio: mode=strict
collected 28 items                                                              

tests/test_pytest.py ............................                  [100%]

=============================== warnings summary ================================
src/database.py:63
  /code/src/database.py:63: MovedIn20Warning: Deprecated API features detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prevent incompatible upgrades prior to updating applications, ensure requirements files are pinned to "sqlalchemy<2.0". Set environment variable SQLALCHEMY_WARN_20=1 to show all deprecation warnings.  Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
    Base = declarative_base()

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
========================= 28 passed, 1 warning in 7.76s =========================

# coverage 리포트 호출
root@c5a7e41c3e84:/code# coverage report -m

#coverage 결과 출력
Name                                  Stmts   Miss  Cover   Missing
-------------------------------------------------------------------
src/__init__.py                           1      0   100%
src/classes/billing.py                  109     65    40%   46-48, 66-133, 136-184, 193, 214-242, 248-263, 267-318
src/classes/general_policy.py           164    114    30%   27-28, 31-35, 46-47, 50-55, 60-61, 65, 70-76, 85-88, 91-111, 114-126, 175-176, 179-180, 185-186, 190, 198-235, 244-245, 251-255, 260-261, 265, 270-272, 281-282, 286-293, 296-300, 303-336, 341-342, 345-353
src/classes/messaging_log.py            176    139    21%   32-50, 56-102, 109-112, 122-133, 141-144, 147-158, 161, 164-172, 186-202, 206, 213-226, 229-239, 249-275, 286-391, 396-418, 423-441, 446-464
src/classes/messaging_setting.py        287     78    73%   42, 49-50, 57, 65, 309, 316, 323, 491-507, 518-522, 525-536, 539-556, 565-566, 573, 582-611, 617-631, 652-656, 679-681, 695-697, 714-716
src/classes/sales_popup.py              381    322    15%   39-57, 60-65, 69, 72-101, 104-138, 143-151, 154-156, 159-181, 184-228, 233-292, 297-361, 366-397, 402-454, 459-460, 493-494, 527-567, 572-618, 623-678, 683-731, 736-791, 804-807, 811-839, 842-867, 879-882, 886-898, 910-913, 916-928
src/classes/send_email.py                60     24    60%   18, 22, 26, 75-77, 80-91, 94, 97-101, 104-107
src/classes/send_messaging_log.py       300    230    23%   40-41, 44-47, 50-57, 62-70, 75-76, 79-99, 102-108, 111-119, 124-125, 128-153, 158-159, 162-170, 173-234, 238, 244-256, 262-276, 281-336, 341-379, 384-386, 389-417, 422-423, 426-431, 436-437, 440-469, 474-475, 479, 483, 488-498, 501-558, 565, 568, 573, 576, 581, 584, 588-600, 604-616
src/config.py                            62      1    98%   13
src/crud.py                            2853   2170    24%   159, 174, 212-227, 231, 253, 301, 307, 338, 343, 350-353, 377-402, 408-416, 420-444, 448-524, 533-700, 704-795, 831-850, 854-873, 900, 921, 945-946, 955-956, 980-1021, 1049-1051, 1065-1071, 1078-1094, 1230, 1248-1270, 1275-1281, 1347-1379, 1383-1453, 1458-1518, 1522-1556, 1559-1566, 1569-1789, 1792-1854, 1864-1890, 1894-1911, 1943, 1950-1951, 1955, 1971-1999, 2010-2016, 2025-2031, 2068-2079, 2083-2127, 2133-2161, 2172-2200, 2209-2216, 2220-2223, 2227-2234, 2242-2250, 2254-2256, 2260-2319, 2323-2328, 2332-2339, 2343-2350, 2354-2359, 2363-2372, 2376-2380, 2384-2386, 2390-2392, 2410-2414, 2418-2420, 2424-2439, 2449-2490, 2500-2530, 2534-2549, 2553-2557, 2561-2564, 2568-2570, 2574-2584, 2600-2695, 2708-2793, 2800-2804, 2817-2833, 2836-2851, 2854-2857, 2860-2866, 2869-2914, 2917-2926, 2934-2942, 2946-2954, 2958-2966, 2972-2979, 2983-2987, 2991-2995, 2999-3003, 3007-3015, 3029-3033, 3050-3060, 3067, 3081-3090, 3094-3102, 3108-3120, 3124-3127, 3131-3152, 3157-3198, 3202-3217, 3221-3256, 3260-3265, 3269-3274, 3278-3283, 3287-3288, 3292-3293, 3297-3298, 3302-3305, 3309-3316, 3322-3333, 3339-3346, 3354-3364, 3373-3403, 3407-3414, 3418-3425, 3429-3436, 3440-3457, 3461-3507, 3522-3764, 3768-3769, 3773-3778, 3797-3834, 3840-3857, 3866-3876, 3913-3916, 3920, 3938, 3946-3963, 3997-4003, 4032-4035, 4047, 4059-4071, 4104-4110, 4145-4163, 4183-4193, 4197-4267, 4271-4273, 4379-4383, 4387-4392, 4397, 4400-4411, 4493-4497, 4501-4504, 4508-4531, 4535-4541, 4545-4549, 4553-4563, 4567-4570, 4574-4582, 4586-4597, 4620-4626, 4637-4655, 4671-4703, 4707-4732, 4739-4754, 4759-4775, 4779-4812, 4816-4825, 4833-4958, 4966-4985, 4989-4998, 5008-5055, 5062-5071, 5076-5121, 5133-5171, 5176-5225, 5236-5279, 5284-5335, 5340-5378, 5387-5399, 5403-5414, 5422-5443, 5452-5474, 5482-5505, 5514-5547, 5554-5570, 5574-5577, 5581-5595, 5599-5611, 5621-5628, 5631-5633, 5636-5647, 5651-5660, 5663-5680, 5683-5687, 5692, 5696, 5702-5711, 5717-5727, 5733-5743, 5747-5756, 5760-5769, 5778-5800, 5807-5828, 5834-5852, 5861-5878, 5887-5900, 5909-5926, 5934-5947, 5957-5991, 5999-6026, 6035-6168, 6174-6303, 6311-6345, 6349-6395, 6401-6438, 6447-6474, 6484-6548, 6558-6592, 6596, 6600, 6618-6736, 6741-6763
src/database.py                          25      2    92%   38-41
src/decorators.py                        30     17    43%   15-23, 27-35, 42
src/dependencies.py                      54     26    52%   18-19, 23-24, 38-42, 52-65, 69-76, 80-87
src/main.py                              55     11    80%   50-51, 56, 98, 103-110
src/models.py                          1121     87    92%   411, 672-679, 683, 688-692, 696, 706, 710, 719-726, 730, 735-739, 743, 782-783, 911-919, 924-925, 929, 938, 942, 1115, 1119, 1212-1213, 1218-1219, 1224-1225, 1230-1231, 1236-1237, 1242-1243, 1248-1249, 1254-1255, 1320-1326, 1330, 1581-1586, 2480-2491, 2495-2515, 2555, 2572, 2576, 2790-2791
src/routers/__init__.py                   0      0   100%
src/routers/auth.py                     370    268    28%   102-166, 176-192, 212, 215, 245, 251, 255, 299, 305, 310, 317-342, 351-361, 370-454, 462-543, 547, 552-578, 586-594, 607-637, 647-666, 673-675, 679, 686-698, 707-715, 729-797
src/routers/billing.py                   61     37    39%   43-57, 69-77, 88-94, 104-115, 126-135, 144-172
src/routers/browse_booster_shops.py     235    154    34%   208, 217-223, 230-241, 248-254, 269-357, 365-377, 392-441, 454-460, 465-466, 470, 477-514, 519, 522, 527-554, 559-561, 565, 575-613, 622-623
src/routers/cart.py                      32     16    50%   48-78
src/routers/customers.py                305    216    29%   58, 86-89, 107, 110, 115-135, 145-159, 178-195, 204-240, 271-299, 308-348, 358-368, 432-435, 441-449, 481-498, 547-610, 615-636, 640-655, 663-675, 688-713, 750-763, 800-812, 819-828, 837-887, 897-903
src/routers/integration.py               18      4    78%   31-32, 41-42
src/routers/messaging.py                261    174    33%   115-165, 173-176, 189-196, 222-231, 255-263, 271-334, 410, 427-428, 445, 460-461, 476-488, 518-549, 566-571, 595-602, 606-617, 629-651, 685-697, 731-766, 779-788, 825-837, 846-849, 857-861, 867-878, 887-904, 929-1002, 1015-1038
src/routers/orders.py                   561    443    21%   147-149, 171-178, 253, 262-306, 310-416, 450-611, 639, 649-689, 708-826, 831-836, 841-849, 854-858, 864-872, 877-880, 885-886, 891-897, 904-929, 936-962, 968-981, 1034-1137, 1150-1159, 1189-1213, 1218-1233, 1255-1277, 1304-1331, 1359-1386, 1391-1394, 1401-1403, 1440-1453, 1494-1507, 1534-1559, 1595-1674, 1683-1707, 1715-1716, 1742-1765, 1788-1811, 1816-1818, 1823-1828, 1833, 1838-1849, 1854-1858, 1866-1875, 1881-1904
src/routers/shops.py                   1305    953    27%   219, 224, 233-248, 253, 274-294, 310-406, 414-424, 439-480, 493-504, 532-544, 548-561, 569-581, 591-611, 623-640, 649-665, 673-707, 722-774, 789-814, 826-850, 865-885, 915-933, 957, 975-981, 996-1004, 1021-1034, 1062-1111, 1132-1151, 1185-1230, 1239-1247, 1255-1306, 1321, 1336-1350, 1358-1398, 1408-1421, 1430-1445, 1453-1454, 1463-1466, 1476-1500, 1505-1512, 1547, 1562, 1585-1596, 1602-1633, 1638-1641, 1652-1676, 1682-1701, 1707-1711, 1720-1762, 1769-1826, 1832-1847, 1859, 1897-1921, 1935-1947, 1956-1971, 1975-1981, 1986-2003, 2016-2044, 2054-2122, 2126-2168, 2172-2194, 2198-2245, 2252-2268, 2272-2310, 2320-2359, 2365-2457, 2467-2475, 2495, 2511-2517, 2526, 2603, 2643-2660, 2667-2687, 2724-2766, 2782-2785, 2814, 2817-2839, 2863-2871, 2880, 2930, 2946-2997, 3008-3023, 3030-3033, 3067-3068, 3078-3081, 3092-3095, 3103-3104, 3114-3120, 3130-3133, 3144-3151, 3161, 3181, 3196-3197, 3207-3251
src/routers/super_admin.py              236    146    38%   161-163, 168-169, 181-241, 248-249, 259-264, 280-290, 307-321, 351-390, 395-396, 405-406, 417-420, 451-489, 496-515, 527-531, 540-542, 552-558, 568-572, 581-583, 593, 609-637, 647-650, 660-665, 675, 684-741
src/routers/webhooks.py                  91     66    27%   69-101, 130-152, 183-204, 237-260, 292-320, 349-358
src/schemas.py                         2627      8    99%   2814-2821
src/shared/analytic.py                  100     90    10%   26-40, 44-58, 61-155, 159-261
src/shared/aws.py                         5      0   100%
src/shared/core_script.py               137    128     7%   14-78, 84-143, 149-195
src/shared/elastic_apm.py                 4      0   100%
src/shared/logger.py                      4      0   100%
src/shared/message_sender.py            127     88    31%   53-58, 62, 66, 78, 88-108, 111-172, 175-193, 199-205, 241-244, 249-265, 268-286, 291, 294, 297
src/shared/messaging_template.py        506    297    41%   19-20, 24, 339-366, 373-407, 411, 415-434, 442, 445-485, 500, 503-543, 558, 561-605, 620, 623-667, 682, 686-722, 736, 739-786, 801, 804-844
src/shared/notion.py                     30     13    57%   15-51, 55-56, 92-93
src/shared/password_hasher.py             2      0   100%
src/shared/redis.py                      38     21    45%   44, 63-72, 90-102, 120-123
src/shared/template.py                    7      0   100%
src/shared/template_filter.py            31     24    23%   7-16, 23-35, 39-45, 49-52
src/shared_module.py                    158    107    32%   50-54, 78-103, 107-119, 125-127, 133-135, 141-143, 149-151, 157-159, 171-176, 180, 184, 188-206, 210-212, 216-218, 223-225, 230-234, 238-245, 283-342, 347-363, 367
tests/__init__.py                         0      0   100%
tests/dummy.py                            2      0   100%
tests/factories.py                      214      2    99%   356-357
tests/test_pytest.py                    250      0   100%
-------------------------------------------------------------------
TOTAL                                 13395   6541    51%

아무래도 유닛테스트다보니 외부 API(Shopify 등)를 호출하는 코드는 테스트 작성이 힘들다. 그와 연관된 파일들이 확실히 커버리지 비율이 상당히 낮다.

회고

이제누가기획해주냐