KyosukeIchikawa commited on
Commit
afb4f81
·
1 Parent(s): 9eac24b

Refactor E2E test setup: consolidate environment setup into conftest.py and remove redundant steps from common_steps.py

Browse files
tests/e2e/conftest.py CHANGED
@@ -1,15 +1,200 @@
1
  """E2Eテスト用のフィクスチャとユーティリティ。
2
 
3
  テスト環境の初期化とページオブジェクトを提供します。
 
4
  """
5
  import os
 
 
6
  import time
 
7
 
8
  import pytest
 
9
  from playwright.sync_api import Browser, Page, sync_playwright
10
 
11
  from tests.utils.logger import test_logger as logger
12
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
  @pytest.fixture(scope="function")
15
  def page(browser: Browser) -> Page:
 
1
  """E2Eテスト用のフィクスチャとユーティリティ。
2
 
3
  テスト環境の初期化とページオブジェクトを提供します。
4
+ 元々 tests/e2e/steps/conftest.py に分かれていた機能を統合しています。
5
  """
6
  import os
7
+ import socket
8
+ import subprocess
9
  import time
10
+ from pathlib import Path
11
 
12
  import pytest
13
+ import requests
14
  from playwright.sync_api import Browser, Page, sync_playwright
15
 
16
  from tests.utils.logger import test_logger as logger
17
 
18
+ # Test data path
19
+ TEST_DATA_DIR = Path(__file__).parent.parent / "data"
20
+
21
+ # Application process
22
+ APP_PROCESS = None
23
+ APP_PORT = None
24
+
25
+
26
+ def find_free_port():
27
+ """
28
+ Find an available port
29
+
30
+ Returns:
31
+ int: Available port number
32
+ """
33
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
34
+ s.bind(("", 0))
35
+ return s.getsockname()[1]
36
+
37
+
38
+ def setup_test_environment():
39
+ """
40
+ バックエンド側のテスト環境をセットアップする
41
+
42
+ Returns:
43
+ int: テストポート番号
44
+ """
45
+ global APP_PROCESS, APP_PORT
46
+
47
+ # Find an available port
48
+ APP_PORT = find_free_port()
49
+
50
+ # Set environment variables
51
+ env = os.environ.copy()
52
+ env["PORT"] = str(APP_PORT)
53
+ env["E2E_TEST_MODE"] = "true"
54
+
55
+ # プロジェクトのルートパスを取得
56
+ project_root = Path(__file__).parent.parent.parent.absolute()
57
+
58
+ # CI環境での仮想環境のPythonパスを設定
59
+ venv_path = os.environ.get("VENV_PATH", "./venv")
60
+ python_executable = os.path.join(venv_path, "bin", "python")
61
+
62
+ # 仮想環境内のPythonが存在しない場合はデフォルトのPythonを使用
63
+ if not os.path.exists(python_executable):
64
+ python_executable = "python"
65
+ logger.warning(
66
+ f"Virtual environment Python not found at {python_executable}, using system Python"
67
+ )
68
+ else:
69
+ logger.info(f"Using Python from virtual environment: {python_executable}")
70
+
71
+ # Launch application as a subprocess
72
+ # 仮想環境のPythonを使用してアプリケーションを起動
73
+ APP_PROCESS = subprocess.Popen(
74
+ [python_executable, "-m", "yomitalk.app"],
75
+ env=env,
76
+ cwd=str(project_root), # プロジェクトルートディレクトリを設定
77
+ stdout=subprocess.PIPE,
78
+ stderr=subprocess.PIPE,
79
+ )
80
+
81
+ # アプリケーション起動を待つために適切な時間を確保
82
+ logger.info(f"Starting application on port {APP_PORT}...")
83
+ time.sleep(3) # 最初に少し長めに待機
84
+
85
+ # Wait for application to start with improved retry logic
86
+ max_retries = 20 # より多くのリトライを許容
87
+ retry_interval = 1.5 # 短いインターバルで頻繁にチェック
88
+
89
+ for i in range(max_retries):
90
+ try:
91
+ # タイムアウト設定を短く
92
+ response = requests.get(f"http://localhost:{APP_PORT}", timeout=2)
93
+ if response.status_code == 200:
94
+ logger.info(f"✓ Application started successfully on port {APP_PORT}")
95
+ # プロセスが稼働中かチェック
96
+ if APP_PROCESS.poll() is None:
97
+ logger.info("Application is running normally")
98
+ return APP_PORT
99
+ else:
100
+ raise Exception(
101
+ f"Application process terminated unexpectedly with code {APP_PROCESS.returncode}"
102
+ )
103
+ except (requests.ConnectionError, requests.Timeout) as e:
104
+ # エラーの種類を詳細に記録
105
+ error_msg = str(e)
106
+ if APP_PROCESS.poll() is not None:
107
+ # プロセスが終了している場合
108
+ stdout, stderr = APP_PROCESS.communicate()
109
+ logger.error(
110
+ f"Application process exited with code {APP_PROCESS.returncode}"
111
+ )
112
+ logger.error(f"stdout: {stdout.decode('utf-8', errors='ignore')}")
113
+ logger.error(f"stderr: {stderr.decode('utf-8', errors='ignore')}")
114
+ raise Exception(
115
+ f"Application process exited prematurely with code {APP_PROCESS.returncode}"
116
+ )
117
+
118
+ logger.info(
119
+ f"Waiting for application to start (attempt {i+1}/{max_retries}): {error_msg[:100]}..."
120
+ )
121
+ time.sleep(retry_interval)
122
+
123
+ # 最終的に失敗した場合
124
+ if APP_PROCESS.poll() is None:
125
+ # プロセスがまだ実行中なら、ログを表示
126
+ logger.error(
127
+ "Application is still running but not responding to HTTP requests."
128
+ )
129
+ else:
130
+ # プロセスが終了している場合
131
+ stdout, stderr = APP_PROCESS.communicate()
132
+ logger.error(f"Application process exited with code {APP_PROCESS.returncode}")
133
+ logger.error(f"stdout: {stdout.decode('utf-8', errors='ignore')}")
134
+ logger.error(f"stderr: {stderr.decode('utf-8', errors='ignore')}")
135
+
136
+ raise Exception("Failed to start application after multiple retries")
137
+
138
+
139
+ def teardown_test_environment():
140
+ """
141
+ テスト環境を終了する
142
+ """
143
+ global APP_PROCESS, APP_PORT
144
+
145
+ if APP_PROCESS:
146
+ logger.info(f"Terminating application process on port {APP_PORT}...")
147
+
148
+ try:
149
+ # まず正常終了を試みる
150
+ APP_PROCESS.terminate()
151
+ try:
152
+ # 終了を待つ(短めのタイムアウト)
153
+ APP_PROCESS.wait(timeout=5)
154
+ except subprocess.TimeoutExpired:
155
+ # 強制終了
156
+ logger.warning(
157
+ "Application did not terminate gracefully, killing process..."
158
+ )
159
+ APP_PROCESS.kill()
160
+ APP_PROCESS.wait(timeout=2)
161
+ except Exception as e:
162
+ logger.error(f"Error during application process termination: {e}")
163
+
164
+ # 状態確認
165
+ if APP_PROCESS.poll() is None:
166
+ logger.warning("WARNING: Application process could not be terminated")
167
+ else:
168
+ logger.info(
169
+ f"Application process terminated with code {APP_PROCESS.returncode}"
170
+ )
171
+
172
+ # リソースをクリア
173
+ APP_PROCESS = None
174
+ APP_PORT = None
175
+
176
+
177
+ @pytest.fixture(scope="session", autouse=True)
178
+ def app_environment():
179
+ """
180
+ バックエンド側のテスト環境を提供するフィクスチャ
181
+
182
+ セッション全体で一度だけアプリケーションを起動し、
183
+ 全テスト終了時に自動的に終了する
184
+ """
185
+ try:
186
+ # バックエンド側のテスト環境のセットアップ
187
+ port = setup_test_environment()
188
+ logger.info(f"Application backend is running on port {port}")
189
+ yield # テスト実行を許可
190
+ except Exception as e:
191
+ # セットアップに失敗した場合の詳細エラー表示
192
+ logger.error(f"ERROR setting up test environment: {e}")
193
+ raise
194
+ finally:
195
+ # 必ず後片付けを実行
196
+ teardown_test_environment()
197
+
198
 
199
  @pytest.fixture(scope="function")
200
  def page(browser: Browser) -> Page:
tests/e2e/steps/common_steps.py CHANGED
@@ -2,27 +2,42 @@
2
  from playwright.sync_api import Page
3
  from pytest_bdd import given
4
 
5
- # Import setup function from conftest
6
- from tests.e2e.steps.conftest import setup_test_environment
7
 
8
 
9
  @given("the application is running")
10
- def application_is_running(page: Page):
11
  """
12
- Set up the application for testing and navigate to it
 
 
 
13
 
14
  Args:
15
  page: Playwright page object
 
16
  """
17
- # Setup test environment and get app port
18
- app_port = setup_test_environment()
 
 
 
 
 
19
 
20
- # Navigate to the application
21
- app_url = f"http://localhost:{app_port}"
 
22
  page.goto(app_url)
23
 
24
- # Wait for the application to load
25
  page.wait_for_load_state("networkidle")
26
 
27
- # Verify that the application has loaded properly
28
- assert page.title() != "", "Application failed to load properly"
 
 
 
 
 
 
2
  from playwright.sync_api import Page
3
  from pytest_bdd import given
4
 
5
+ # conftest.pyから必要な関数やフィクスチャをインポート
6
+ from tests.utils.logger import test_logger as logger
7
 
8
 
9
  @given("the application is running")
10
+ def application_is_running(page: Page, app_environment):
11
  """
12
+ フロントエンド側のテスト:ブラウザでアプリケーションにアクセスする
13
+
14
+ Note: バックエンド側のアプリケーションは app_environment フィクスチャによって
15
+ 既に起動されています(tests/e2e/conftest.py で定義)
16
 
17
  Args:
18
  page: Playwright page object
19
+ app_environment: バックエンドのフィクスチャ
20
  """
21
+ # conftest.pyから直接APP_PORTを取得(モジュールレベルの変数)
22
+ from tests.e2e.conftest import APP_PORT
23
+
24
+ # アプリケーションが起動していることを確認
25
+ assert (
26
+ APP_PORT is not None
27
+ ), "Application port is not set. Test environment might not be properly initialized."
28
 
29
+ # ブラウザでアプリケーションにアクセス
30
+ logger.info(f"Opening application in browser at http://localhost:{APP_PORT}")
31
+ app_url = f"http://localhost:{APP_PORT}"
32
  page.goto(app_url)
33
 
34
+ # ページの読み込み完了を待つ
35
  page.wait_for_load_state("networkidle")
36
 
37
+ # 重要なUI要素が表示されるのを待つ
38
+ page.wait_for_selector("h1, h2", timeout=5000) # 見出し要素を待つ
39
+
40
+ # ページが正しく読み込まれたことを検証
41
+ title = page.title()
42
+ assert title != "", "Application failed to load properly"
43
+ logger.info(f"Successfully loaded application in browser with title: {title}")
tests/e2e/steps/conftest.py DELETED
@@ -1,245 +0,0 @@
1
- """E2E test common environment settings module.
2
-
3
- Provides setup for test environment and common steps.
4
- """
5
- import os
6
- import socket
7
- import subprocess
8
- import time
9
- from pathlib import Path
10
-
11
- import pytest
12
- import requests
13
- from playwright.sync_api import Page
14
- from pytest_bdd import given
15
-
16
- from tests.utils.logger import test_logger as logger
17
-
18
- # Test data path
19
- TEST_DATA_DIR = Path(__file__).parent.parent.parent / "data"
20
-
21
- # Application process
22
- APP_PROCESS = None
23
- APP_PORT = None
24
-
25
-
26
- def find_free_port():
27
- """
28
- Find an available port
29
-
30
- Returns:
31
- int: Available port number
32
- """
33
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
34
- s.bind(("", 0))
35
- return s.getsockname()[1]
36
-
37
-
38
- def setup_test_environment():
39
- """
40
- Set up the test environment
41
-
42
- Returns:
43
- int: Test port number
44
- """
45
- global APP_PROCESS, APP_PORT
46
-
47
- # Find an available port
48
- APP_PORT = find_free_port()
49
-
50
- # Set environment variables
51
- env = os.environ.copy()
52
- env["PORT"] = str(APP_PORT)
53
- env["E2E_TEST_MODE"] = "true"
54
-
55
- # プロジェクトのルートパスを取得
56
- project_root = Path(__file__).parent.parent.parent.parent.absolute()
57
-
58
- # CI環境での仮想環境のPythonパスを設定
59
- venv_path = os.environ.get("VENV_PATH", "./venv")
60
- python_executable = os.path.join(venv_path, "bin", "python")
61
-
62
- # 仮想環境内のPythonが存在しない場合はデフォルトのPythonを使用
63
- if not os.path.exists(python_executable):
64
- python_executable = "python"
65
- logger.warning(
66
- f"Virtual environment Python not found at {python_executable}, using system Python"
67
- )
68
- else:
69
- logger.info(f"Using Python from virtual environment: {python_executable}")
70
-
71
- # Launch application as a subprocess
72
- # 仮想環境のPythonを使用してアプリケーションを起動
73
- APP_PROCESS = subprocess.Popen(
74
- [python_executable, "-m", "yomitalk.app"],
75
- env=env,
76
- cwd=str(project_root), # プロジェクトルートディレクトリを設定
77
- stdout=subprocess.PIPE,
78
- stderr=subprocess.PIPE,
79
- )
80
-
81
- # アプリケーション起動を待つために適切な時間を確保
82
- logger.info(f"Starting application on port {APP_PORT}...")
83
- time.sleep(3) # 最初に少し長めに待機
84
-
85
- # Wait for application to start with improved retry logic
86
- max_retries = 20 # より多くのリトライを許容
87
- retry_interval = 1.5 # 短いインターバルで頻繁にチェック
88
-
89
- for i in range(max_retries):
90
- try:
91
- # タイムアウト設定を短く
92
- response = requests.get(f"http://localhost:{APP_PORT}", timeout=2)
93
- if response.status_code == 200:
94
- logger.info(f"✓ Application started successfully on port {APP_PORT}")
95
- # プロセス出力を非ブロッキングでチェック (communicate()は使わない)
96
- # プロセスが稼働中かチェック
97
- if APP_PROCESS.poll() is None:
98
- logger.info("Application is running normally")
99
- return APP_PORT
100
- else:
101
- raise Exception(
102
- f"Application process terminated unexpectedly with code {APP_PROCESS.returncode}"
103
- )
104
- except (requests.ConnectionError, requests.Timeout) as e:
105
- # エラーの種類を詳細に記録
106
- error_msg = str(e)
107
- if APP_PROCESS.poll() is not None:
108
- # プロセスが終了している場合
109
- stdout, stderr = APP_PROCESS.communicate()
110
- logger.error(
111
- f"Application process exited with code {APP_PROCESS.returncode}"
112
- )
113
- logger.error(f"stdout: {stdout.decode('utf-8', errors='ignore')}")
114
- logger.error(f"stderr: {stderr.decode('utf-8', errors='ignore')}")
115
- raise Exception(
116
- f"Application process exited prematurely with code {APP_PROCESS.returncode}"
117
- )
118
-
119
- logger.info(
120
- f"Waiting for application to start (attempt {i+1}/{max_retries}): {error_msg[:100]}..."
121
- )
122
- time.sleep(retry_interval)
123
-
124
- # 最終的に失敗した場合
125
- if APP_PROCESS.poll() is None:
126
- # プロセスがまだ実行中なら、ログを表示
127
- logger.error(
128
- "Application is still running but not responding to HTTP requests."
129
- )
130
- else:
131
- # プロセスが終了している場合
132
- stdout, stderr = APP_PROCESS.communicate()
133
- logger.error(f"Application process exited with code {APP_PROCESS.returncode}")
134
- logger.error(f"stdout: {stdout.decode('utf-8', errors='ignore')}")
135
- logger.error(f"stderr: {stderr.decode('utf-8', errors='ignore')}")
136
-
137
- raise Exception("Failed to start application after multiple retries")
138
-
139
-
140
- def teardown_test_environment():
141
- """
142
- テスト環境を終了する
143
- """
144
- global APP_PROCESS, APP_PORT
145
-
146
- if APP_PROCESS:
147
- logger.info(f"Terminating application process on port {APP_PORT}...")
148
-
149
- try:
150
- # まず正常終了を試みる
151
- APP_PROCESS.terminate()
152
- try:
153
- # 終了を待つ(短めのタイムアウト)
154
- APP_PROCESS.wait(timeout=5)
155
- except subprocess.TimeoutExpired:
156
- # 強制終了
157
- logger.warning(
158
- "Application did not terminate gracefully, killing process..."
159
- )
160
- APP_PROCESS.kill()
161
- APP_PROCESS.wait(timeout=2)
162
- except Exception as e:
163
- logger.error(f"Error during application process termination: {e}")
164
-
165
- # 状態確認
166
- if APP_PROCESS.poll() is None:
167
- logger.warning("WARNING: Application process could not be terminated")
168
- else:
169
- logger.info(
170
- f"Application process terminated with code {APP_PROCESS.returncode}"
171
- )
172
-
173
- # リソースをクリア
174
- APP_PROCESS = None
175
- APP_PORT = None
176
-
177
-
178
- @pytest.fixture(scope="session", autouse=True)
179
- def app_environment():
180
- """
181
- テスト環境を提供するフィクスチャ
182
- """
183
- try:
184
- # テスト環境のセットアップを試行
185
- setup_test_environment()
186
- yield # テスト実行を許可
187
- except Exception as e:
188
- # セットアップに失敗した場合の詳細エラー表示
189
- logger.error(f"ERROR setting up test environment: {e}")
190
- raise
191
- finally:
192
- # 必ず後片付けを実行
193
- teardown_test_environment()
194
-
195
-
196
- @given("the application is running")
197
- def app_is_running(page: Page):
198
- """
199
- Verify that the application is running and navigate to the application page
200
-
201
- This step also navigates to the application page, making it a common entry point
202
- for all test scenarios.
203
-
204
- Args:
205
- page: Playwright page object
206
- """
207
- assert APP_PROCESS is not None, "Application is not running"
208
-
209
- # Application health check
210
- if APP_PROCESS.poll() is not None:
211
- stdout, stderr = APP_PROCESS.communicate()
212
- pytest.fail(
213
- f"Application process exited with code {APP_PROCESS.returncode}\n"
214
- f"stdout: {stdout.decode('utf-8', errors='ignore')}\n"
215
- f"stderr: {stderr.decode('utf-8', errors='ignore')}"
216
- )
217
-
218
- # Navigate to the application page with retry logic
219
- max_retries = 3
220
- last_exception = None
221
-
222
- for i in range(max_retries):
223
- try:
224
- # Navigate with timeout
225
- page.goto(f"http://localhost:{APP_PORT}", timeout=20000)
226
-
227
- # Wait for critical elements to load
228
- page.wait_for_selector("h1, h2", timeout=5000) # Wait for headings
229
-
230
- # Verify the page loaded successfully
231
- title = page.title()
232
- assert title != "", "Failed to load the application page - empty title"
233
- logger.info(f"Successfully loaded application with title: {title}")
234
- return # Success - return early
235
- except Exception as e:
236
- last_exception = e
237
- logger.warning(
238
- f"Retry {i+1}/{max_retries}: Failed to connect to the application: {e}"
239
- )
240
- time.sleep(2) # Wait before retrying
241
-
242
- # All retries failed
243
- pytest.fail(
244
- f"Failed to connect to the application after {max_retries} attempts: {last_exception}"
245
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tests/e2e/steps/file_upload_steps.py CHANGED
@@ -3,7 +3,7 @@
3
  from playwright.sync_api import Page, expect
4
  from pytest_bdd import parsers, then, when
5
 
6
- from tests.e2e.steps.conftest import TEST_DATA_DIR
7
 
8
 
9
  @when(parsers.parse('I upload a {file_type} file "{file_name}"'))
 
3
  from playwright.sync_api import Page, expect
4
  from pytest_bdd import parsers, then, when
5
 
6
+ from tests.e2e.conftest import TEST_DATA_DIR
7
 
8
 
9
  @when(parsers.parse('I upload a {file_type} file "{file_name}"'))