PythonでRequestsを使ったクラスのテストを書きたい

## 動機
* [Requests](https://requests.readthedocs.io/en/latest/)でwebページを取得し[BeautifulSoup](https://beautiful-soup-4.readthedocs.io/en/latest/)で内容を取得するクラスのテストを行いたい

## テストの手法
* pythonの[http.server](https://docs.python.org/ja/3/library/http.server.html)を利用してサーバを起動
    * 以下のコマンドを実行するとhttpサーバが立ち上がる
    ```bash
     python -m http.server --bind 127.0.0.1 8000
    ```
* 各テストの前にテスト用ファイルを作成
* UseRequestsClassのメソッドを実行
    * 作成したファイルにrequest.get

## 使用ライブラリ
* python 3.12.0
* pip 23.2.1
```text:requirements.txt
requests==2.31.0
beautifulsoup4==4.12.2
pytest==8.0.0
```
## プログラム
* GitHubリポジトリのリンク -[py_requests_test](https://github.com/Hikakkun/py_requests_test)
### UseRequestsClass(テスト対象のクラス)
* urlからrequestsでwebページを取得
* execute(css_selector)では引数のcss_selectorのtextを取得
```python:use_requests_class.py
from dataclasses import dataclass
import requests
from requests.exceptions import HTTPError, Timeout, RequestException
from bs4 import BeautifulSoup

@dataclass
class UseRequestsClass:
    """
    HTTPリクエストを行い、指定されたCSSセレクタに基づいてHTML要素を取得するクラス。

    Attributes:
        url (str): リクエストを行う対象のURL
    """
    url : str 
    
    def execute(self, css_selector : str) -> str | None:
        """
        指定されたCSSセレクタに基づいてHTML要素を取得します。

        Args:
            css_selector (str): 取得するHTML要素を指定するCSSセレクタ

        Returns:
            str | None: 取得したHTML要素のテキスト。エラーが発生した場合はNone。
        """
        try:
            response = requests.get(self.url)
            response.raise_for_status()
        except HTTPError as http_err:
            print(f'HTTPエラーが発生しました: {http_err}')

        except Timeout as timeout_err:
            print(f'タイムアウトエラーが発生しました: {timeout_err}')

        except RequestException as req_err:
            print(f'リクエストエラーが発生しました: {req_err}')

        except Exception as err:
            print(f'予期せぬエラーが発生しました: {err}')

        else:
            # エラーが発生しなかった場合の処理
            soup = BeautifulSoup(response.content.decode('utf-8'), 'html.parser')
            search_element_soup = soup.select_one(css_selector)
            return search_element_soup.get_text("")
        
        return None
        
```

### tests
```python:tests/test_use_requwsts_class.py
import pytest
from conftest import UseRequestsClass
import subprocess
import time
import socket
import urllib.parse
import os

SCRIPT_DIRECTORY = os.path.dirname(os.path.abspath(__file__))
HOST = '127.0.0.1'
PORT = 8000
CREATE_FILE_PATH_LIST = []
def get_request_base_url():
    """
    リクエストのベースURLを取得します。

    Returns:
        str: ベースURL
    """
    return f"http://{HOST}:{PORT}"

@pytest.fixture(scope="session")
def setup_server():
    """
    テスト用のHTTPサーバーをセットアップします。
    セッションスコープのフィクスチャとして使用されます。
    """
    #サーバを起動
    server_process = subprocess.Popen(['python', '-m', 'http.server', '--directory', SCRIPT_DIRECTORY,'--bind', HOST, str(PORT)])
    wait_for_server()
    yield
    #サーバを終了
    server_process.terminate() 
    server_process.wait()
    #テストで作成したファイルを削除
    for file_path in CREATE_FILE_PATH_LIST:
        os.remove(file_path)

def wait_for_server():
    """
        サーバが起動するまで待機する関数    
    """
    max_attempts = 30  # 最大で30回試行
    wait_time = 1  # 待機時間（秒）

    for _ in range(max_attempts):
        with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
            try:
                s.connect((HOST, PORT))
                break
            except (ConnectionRefusedError, OSError):
                time.sleep(wait_time)    

def create_file(file_name : str, file_content : str):
    """
    指定されたファイル名と内容でファイルを作成し、作成したファイルパスをリストに追加します。

    Args:
        file_name (str): 作成するファイルの名前
        file_content (str): ファイルに書き込む内容
    """
    create_file_path = os.path.join(SCRIPT_DIRECTORY, file_name)
    with open(create_file_path, 'w', encoding='utf-8') as file:
        file.write(file_content)
        CREATE_FILE_PATH_LIST.append(create_file_path)

def test_html(setup_server):
    #正常なHTMLファイルに対して正しい結果が得られることを検証します。
    file_name = "title.html"
    file_content = r"""
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>テスト用のHTML</title>
        </head>
        <body>
            <h1>Hello, World!</h1>
            <p>This is a simple HTML document for testing purposes.</p>
        </body>
        </html>    
    """
    create_file(file_name, file_content)
    request_url = urllib.parse.urljoin(get_request_base_url(), file_name)
    use_requests_class = UseRequestsClass(request_url)
    assert use_requests_class.execute("title") == "テスト用のHTML"
    assert use_requests_class.execute("body h1") == "Hello, World!"
    assert use_requests_class.execute("body p") == "This is a simple HTML document for testing purposes."

def test_request_exception(setup_server):
    #存在しないファイルへのリクエストに対してNoneが返ることを検証します。
    request_url = urllib.parse.urljoin(get_request_base_url(), "non_existent_file")
    use_requests_class = UseRequestsClass(request_url)
    assert use_requests_class.execute("title") == None
```