duckdb 와 codeception / php behavior test / BDD in php
DuckDB + Codeception 기반 BDD 테스트 환경 설정 방법
1. .env.testing
설정
.env.testing
파일을 아래와 같이 설정합니다:
APP_ENV=testing
DB_CONNECTION=duckdb
DB_DATABASE=dusk_test.duckdb
CHROMEDRIVER_PORT=9515
TEST_SERVER_PORT=8001
DUSK_SERVER_PORT=8081
SESSION_DRIVER=file
CACHE_DRIVER=file
2. DuckDB 테스트 환경 구성
tests/TestCase.php
에서 DuckDB 테스트 환경을 구성합니다:
<?php
namespace Tests;
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
abstract class TestCase extends BaseTestCase
{
protected function setUp(): void
{
parent::setUp();
// DuckDB 테스트 데이터베이스 파일 생성
$testDbPath = base_path(env('DB_DATABASE', 'dusk_test.duckdb'));
if (!file_exists($testDbPath)) {
touch($testDbPath);
}
// DB 연결 설정
config(['database.connections.duckdb.database' => $testDbPath]);
config(['database.default' => 'duckdb']);
// DuckDB 서비스 프로바이더 등록
$this->app->register(\App\Providers\TestDuckDBServiceProvider::class);
// 마이그레이션 실행
if (method_exists($this, 'artisan')) {
$this->artisan('migrate:fresh', [
'--database' => 'duckdb',
'--path' => 'database/duckdb_migrations'
]);
}
}
}
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use Illuminate\Database\DatabaseManager;
class TestDuckDBServiceProvider extends ServiceProvider
{
public function register()
{
// 테스트 환경에서만 DuckDB 드라이버 등록
if ($this->app->environment('testing')) {
$this->app->resolving('db', function (DatabaseManager $db) {
$db->extend('duckdb', function ($config, $name) {
// DuckDBService를 통해 DuckDB 인스턴스 생성
$duckdb = app(\App\Services\DuckDBService::class)->createConnection($config);
// 커스텀 DuckDB 연결 클래스로 래핑
return new \App\Database\Connections\DuckDBConnection($duckdb, $config['database'] ?? '', '', $config);
});
});
}
}
}
<?php
namespace App\Database\Connections;
use Illuminate\Database\Connection;
use Saturio\DuckDB\DuckDB as BaseDuckDB;
use Illuminate\Database\Query\Processors\Processor;
class DuckDBConnection extends Connection
{
protected $duckdb;
public function __construct(BaseDuckDB $duckdb, $database = '', $tablePrefix = '', array $config = [])
{
$this->duckdb = $duckdb;
parent::__construct($duckdb, $database, $tablePrefix, $config);
}
protected function getDefaultQueryGrammar()
{
return $this->getQueryGrammar();
}
protected function getDefaultSchemaGrammar()
{
return $this->getSchemaGrammar();
}
protected function getDefaultPostProcessor()
{
return new Processor();
}
public function getDuckDB()
{
return $this->duckdb;
}
public function select($query, $bindings = [], $useReadPdo = true)
{
return $this->executeQuery($query, $bindings);
}
public function insert($query, $bindings = [])
{
return $this->executeQuery($query, $bindings);
}
public function update($query, $bindings = [])
{
return $this->executeQuery($query, $bindings);
}
public function delete($query, $bindings = [])
{
return $this->executeQuery($query, $bindings);
}
public function getName()
{
return $this->getConfig('name') ?: 'duckdb';
}
public function executeQuery($sql, $params = [])
{
if (empty($params)) {
return $this->duckdb->query($sql);
}
$prepared = $this->duckdb->preparedStatement($sql);
foreach ($params as $index => $param) {
$prepared->bindParam($index + 1, $param);
}
return $prepared->execute();
}
}
3. Codeception 설정
codeception.yml
설정
paths:
tests: tests
output: tests/_output
data: tests/Support/Data
support: tests/Support
envs: tests/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
tests/acceptance.suite.yml
설정
actor: AcceptanceTester
modules:
enabled:
- WebDriver:
url: "http://localhost:8080/" # EnvWebDriver에서 재정의된다.
browser: 'chrome'
host: '127.0.0.1'
port: "9515" # EnvWebDriver에서 재정의된다.
path: ''
capabilities:
chromeOptions:
args: ["--no-sandbox", "--disable-dev-shm-usage"]
- Support\Helper\EnvWebDriver
step_decorators:
- Codeception\Step\ConditionalAssertion
- Codeception\Step\TryTo
- Codeception\Step\Retry
4. WebDriver 환경변수 동적 주입
tests/Support/Helper/EnvWebDriver.php
에서 acceptance.suite.yml
에 환경변수를 동적으로 주입합니다:
class EnvWebDriver extends Module
{
public function _beforeSuite($settings = [])
{
file_put_contents(codecept_output_dir() . 'envwebdriver.log', print_r($_ENV, true));
}
public function _initialize()
{
$testServerPort = getenv('TEST_SERVER_PORT') ?: '8080';
$port = getenv('CHROMEDRIVER_PORT') ?: '9515';
$url = 'http://localhost' . ':' . $testServerPort . '/';
if ($this->hasModule('WebDriver')) {
$wd = $this->getModule('WebDriver');
$wd->_setConfig(['url' => $url, 'port' => $port]);
}
}
}
5. 테스트 코드 예시
Acceptance 테스트 - tests/Acceptance/FinancialTradesCest.php
public function tradesTablePaginationWorks(AcceptanceTester $I)
{
$I->amOnPage('/duckdb/financial-trades');
$I->waitForElementVisible('#tab-trades', 5);
$I->click('#tab-trades');
$I->waitForElementVisible('#trades-container', 5);
$tradeIdIndex = 0;
$firstTradeId = $I->executeJS("
const idx = $tradeIdIndex;
const firstRow = document.querySelector('#trades-container tbody tr:first-child');
if (!firstRow || idx < 0) return null;
const cells = firstRow.querySelectorAll('td');
return cells[idx]?.textContent.trim() ?? null;
");
\PHPUnit\Framework\Assert::assertNotEmpty($firstTradeId);
$paginationSelector = "//a[contains(@href, 'page=2') and normalize-space(text()) ='2']";
$I->waitForElementClickable(By::xpath($paginationSelector), 5);
$I->click(By::xpath($paginationSelector));
$I->waitForText('Loading...', 5, '#content-trades');
$I->waitForElementNotVisible(By::xpath("//div[@id='content-trades' and contains(text(), 'Loading...')]"), 20);
$I->wait(10);
$secondTradeId = $I->executeJS("
const idx = $tradeIdIndex;
const firstRow = document.querySelector('#trades-container tbody tr:first-child');
if (!firstRow || idx < 0) return null;
const cells = firstRow.querySelectorAll('td');
return cells[idx]?.textContent.trim() ?? null;
");
assertNotEquals($firstTradeId, $secondTradeId);
}
6. 실행
.\run-acceptance-test.ps1
Main
을 확인하자
# ChromeDriver 자동 설치 및 테스트 실행 스크립트 (Windows PowerShell)
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# 변수 선언
$envFilePath = ".env.testing"
$chromedriverPath = ".\chromedriver-win64\chromedriver.exe"
$zipUrl = "https://storage.googleapis.com/chrome-for-testing-public/140.0.7339.80/win64/chromedriver-win64.zip"
$zipFile = "chromedriver-win64.zip"
$extractPath = "."
function Print-Info($msg) {
Write-Host "[INFO] $msg"
}
function Print-Warn($msg) {
Write-Warning "[WARN] $msg"
}
function Load-EnvVariables($EnvFilePath) {
if (Test-Path $EnvFilePath) {
Get-Content $EnvFilePath | ForEach-Object {
$line = $_.Trim()
if ($line -and !$line.StartsWith('#') -and $line.Contains('=')) {
$key, $value = $line.Split('=', 2)
$key = $key.Trim()
$value = $value.Trim().Trim('"').Trim("'")
Set-Item -Path "Env:\$key" -Value $value
}
}
Print-Info "환경 변수가 로드되었습니다.($EnvFilePath)"
} else {
Print-Warn "Could not find .env file at '$EnvFilePath'."
}
}
function Install-ChromeDriver() {
if (-Not (Test-Path $chromedriverPath)) {
Print-Info "ChromeDriver가 없습니다. 다운로드 및 설치를 시작합니다..."
Invoke-WebRequest -Uri $zipUrl -OutFile $zipFile
Print-Info "압축 해제 중..."
Expand-Archive -Path $zipFile -DestinationPath $extractPath
Print-Info "압축 파일 삭제 중..."
Remove-Item $zipFile
Print-Info "설치 완료!"
} else {
Print-Info "ChromeDriver가 이미 설치되어 있습니다."
}
}
function Start-ChromeDriver($chromedriverPort) {
if (Get-Process -Name "chromedriver" -ErrorAction SilentlyContinue) {
Print-Info "ChromeDriver가 이미 실행 중입니다."
} else {
Print-Info "ChromeDriver가 실행 중이 아닙니다. 테스트를 시작합니다."
Start-Process -NoNewWindow -FilePath $chromedriverPath -ArgumentList "--port=$chromedriverPort"
Start-Sleep -Seconds 10
}
}
function Seed-Database() {
Print-Info "테스트용 DB 시드 데이터 입력 중..."
$env:APP_ENV="testing"; php artisan seed:financial-trades
}
function Start-TestServer($testServerPort) {
Print-Info "테스트용 웹서버를 실행합니다 (php artisan serve --env=testing --port=$testServerPort)..."
Start-Process -NoNewWindow -FilePath "php" -ArgumentList "artisan serve --env=testing --port=$testServerPort"
Start-Sleep -Seconds 5
}
function Run-AcceptanceTest() {
Print-Info "Codeception acceptance 테스트 실행 중..."
vendor\bin\codecept run acceptance
}
function Main() {
Load-EnvVariables $envFilePath
$chromedriverPort = $env:CHROMEDRIVER_PORT
$testServerPort = $env:TEST_SERVER_PORT
Install-ChromeDriver
Start-ChromeDriver $chromedriverPort
Seed-Database
Start-TestServer $testServerPort
Run-AcceptanceTest
}
Main
Main 의 주요 단계 설명
- 환경변수 로드 :
.env.testing
파일을 읽어 PowerShell 환경변수로 설정합니다.Load-EnvVariables $envFilePath
- 포트 변수 추출 : 환경변수에서 ChromeDriver와 테스트 서버 포트를 가져옵니다.
$chromedriverPort = $env:CHROMEDRIVER_PORT
$testServerPort = $env:TEST_SERVER_PORT
- ChromeDriver 설치 : 없으면 자동 다운로드 및 압축 해제.
Install-ChromeDriver
- ChromeDriver 실행 : 지정 포트로 프로세스 실행.
Start-ChromeDriver $chromedriverPort
- DB 시드 데이터 입력 : artisan 커맨드로 DuckDB에 거래 데이터 입력.
Seed-Database
- 테스트용 웹서버 실행
: php artisan serve
로 테스트 서버 기동.Start-TestServer $testServerPort
- Codeception acceptance 테스트 실행
Run-AcceptanceTest
결과 확인
테스트 결과는 tests/_output/
폴더에서 확인할 수 있습니다.
댓글 없음:
댓글 쓰기