CodeIgniter 4로 블로그 만들기 #04 — 테스트 환경 점검과 첫 테스트
지금까지 세 회차 동안 프로젝트를 만들고, 라우트 → 컨트롤러 → 뷰 흐름으로 about 페이지를 만들고, 공통 레이아웃과 디자인을 입혔습니다. 브라우저로 열어 보며 "잘 되네"를 눈으로 확인해 왔죠.
그런데 앞으로 기능이 늘어나면, 매번 손으로 모든 페이지를 열어 확인하기가 어려워집니다. 글 저장 기능을 고쳤는데 about 페이지가 깨지지 않았는지, 로그인 로직을 바꿨는데 목록이 여전히 뜨는지… 이런 걸 자동으로 지켜 줄 안전망이 필요합니다. 그게 테스트입니다.
이 강좌는 이후 여러 회차에서 테스트를 먼저 쓰고(빨강) → 구현으로 통과시키는(초록) TDD 방식으로 진행합니다. 그 첫걸음으로, 이번 회차에서는 테스트 환경이 이미 준비돼 있음을 확인하고, FeatureTestTrait로 엔드포인트를 실제로 호출하는 첫 Feature 테스트를 작성합니다.
이번 회차 목표
- appstarter가 기본 제공하는 테스트 환경(테스트 DB, PHPUnit 설정) 점검하기
FeatureTestTrait::call()로 라우트를 호출하는 Feature 테스트 이해하기- about 페이지에 대한 첫 스모크 테스트 작성하고 통과시키기
1. 테스트 환경은 이미 준비돼 있다
좋은 소식이 있습니다. appstarter에는 테스트에 필요한 것들이 이미 갖춰져 있습니다. 이번 회차에서 설정 파일을 새로 만들 필요가 거의 없다는 뜻입니다. 두 가지만 확인하고 넘어갑니다.
(1) 테스트용 데이터베이스 — SQLite 메모리
app/Config/Database.php를 열면 tests라는 데이터베이스 그룹이 기본으로 들어 있습니다.
public array $tests = [
'DSN' => '',
'hostname' => '127.0.0.1',
'username' => '',
'password' => '',
'database' => ':memory:',
'DBDriver' => 'SQLite3',
'DBPrefix' => 'db_',
// ...
];
핵심은 'database' => ':memory:'와 'DBDriver' => 'SQLite3'입니다. 테스트를 돌릴 때는 메모리 위에 뜨는 SQLite를 씁니다. 파일도 서버도 필요 없고, 테스트가 끝나면 흔적 없이 사라집니다. 그래서 실제 개발/운영 DB(MySQL 등)를 절대 건드리지 않고 안전하게 테스트할 수 있습니다.
같은 파일 아래쪽에는 이런 장치도 있습니다.
if (ENVIRONMENT === 'testing') {
$this->defaultGroup = 'tests';
}
환경이 testing이면 자동으로 tests 그룹을 기본 DB로 삼는다는 뜻입니다. PHPUnit이 돌 때 CI4는 환경을 testing으로 잡으므로, 우리가 따로 지정하지 않아도 테스트는 알아서 메모리 DB를 씁니다.
(2) PHPUnit 설정
appstarter에는 phpunit.xml.dist와 PHPUnit 자체(vendor)가 이미 포함돼 있습니다. ep01에서 확인했던 composer test 스크립트도 그대로 살아 있죠. 그래서 이번 회차에서 설정 파일은 손대지 않습니다. 커밋 메시지에도 적혀 있듯, "tests 그룹·phpunit 설정은 기본값이 이미 충족하여 변경 없음"입니다.
즉 이번 회차의 실질적인 변경은 테스트 파일 하나 추가뿐입니다. 나머지는 "이미 잘 갖춰져 있음을 확인"하는 과정입니다.
2. Feature 테스트란
CI4의 테스트는 크게 두 결이 있습니다.
- Unit(단위) 테스트 — 함수/메서드 하나를 떼어 내 검증.
- Feature 테스트 — 실제 라우트를 호출해서 컨트롤러 → 뷰까지의 전체 흐름을 검증.
우리가 지금 확인하고 싶은 건 "/about 주소가 제대로 응답하고, 화면에 제목이 나오는가"입니다. 이건 라우트·컨트롤러·뷰가 다 맞물려야 하는 일이므로 Feature 테스트가 딱 맞습니다.
Feature 테스트의 핵심 도구가 FeatureTestTrait입니다. 이 트레잇을 쓰면 테스트 안에서 $this->call('GET', 'about')처럼 가짜 요청을 앱에 던지고, 돌아온 응답을 검사할 수 있습니다. 브라우저 없이, 코드로 라우트를 두드리는 셈입니다.
3. 첫 테스트 작성
tests/Feature/PagesTest.php를 만듭니다.
<?php
namespace Tests\Feature;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
/**
* 정적 페이지에 대한 첫 Feature 테스트.
*
* FeatureTestTrait::call()로 실제 라우트를 호출해
* 컨트롤러 → 뷰까지의 흐름이 정상 동작하는지 검증한다.
*/
final class PagesTest extends CIUnitTestCase
{
use FeatureTestTrait;
public function testAboutPageReturns200(): void
{
$result = $this->call('GET', 'about');
$result->assertStatus(200);
$result->assertOK();
}
public function testAboutPageShowsHeading(): void
{
$result = $this->call('GET', 'about');
// 공통 레이아웃 위에 본문 섹션이 끼워져 렌더링되는지 확인
$result->assertSee('소개', 'h1');
}
}
하나씩 뜯어봅니다.
namespace Tests\Feature;— 테스트는tests/아래,Tests\네임스페이스를 씁니다. 폴더 구조와 맞춰Feature를 붙였습니다.extends CIUnitTestCase— CI4가 제공하는 테스트 기반 클래스. 프레임워크를 테스트용으로 부팅해 줍니다.use FeatureTestTrait;— 앞서 말한call()을 쓰기 위한 트레잇.$this->call('GET', 'about')—/about으로 GET 요청을 보내고, 결과를$result에 담습니다.$result에는assert…검증 메서드들이 붙어 있습니다.
두 개의 테스트가 확인하는 것
첫 번째 testAboutPageReturns200 — 응답 상태를 봅니다.
assertStatus(200)— HTTP 상태 코드가 200(정상)인가.assertOK()— 성공 범위(2xx)의 응답인가.assertStatus(200)과 겹치지만, "정상 응답"을 명시적으로 한 번 더 못 박는 스모크 테스트입니다.
라우트가 없거나 컨트롤러가 예외를 던지면 이 테스트가 실패합니다. 즉 "about 페이지가 살아 있는가" 를 지켜 줍니다.
두 번째 testAboutPageShowsHeading — 화면 내용을 봅니다.
assertSee('소개', 'h1')— 응답 HTML의<h1>태그 안에 "소개"라는 글자가 보이는가.
이건 단순히 200이 떨어지는 걸 넘어, 뷰가 실제로 렌더링됐는지를 확인합니다. 특히 ep03에서 about 페이지를 레이아웃 상속(extend/section) 구조로 바꿨는데, 그 구조가 제대로 조립돼 <h1 class="page-title">소개</h1>까지 그려지는지를 검증하는 것입니다. 레이아웃이 깨지면 <h1>이 안 나올 테고, 이 테스트가 잡아 줍니다.
4. 테스트 실행
ep01에서 등록해 둔 스크립트로 돌립니다.
composer test
특정 파일만 돌리려면:
./vendor/bin/phpunit tests/Feature/PagesTest.php
두 테스트가 초록(통과)으로 뜨면 성공입니다. 앞선 회차에서 about 페이지와 레이아웃을 이미 제대로 만들어 두었기 때문에, 이번 테스트는 바로 통과합니다.
TDD 관점의 팁: 원리를 체감하고 싶다면, 잠깐
Routes.php에서 about 라우트를 주석 처리하고 테스트를 돌려 보세요.testAboutPageReturns200이 빨강으로 실패하는 걸 볼 수 있습니다(404 응답). 확인 후 주석을 되돌려 다시 초록으로 만들면, "빨강 → 초록"의 감을 잡을 수 있습니다. 이후 기능 회차들은 이 순서를 진짜로 밟습니다 — 실패하는 테스트를 먼저 쓰고, 구현으로 통과시킵니다.
핵심 개념 — 왜 지금 테스트를 깔아 두나
기능이 거의 없는 지금 테스트를 도입하는 데는 이유가 있습니다.
- 안전망을 먼저 친다 — 앞으로 글 CRUD, 인증, 댓글처럼 서로 얽히는 기능이 쏟아집니다. 하나를 고칠 때 다른 게 깨지지 않았는지를 사람이 매번 확인하는 건 비현실적입니다. 테스트가 그걸 자동으로 지켜 줍니다.
- 엔드포인트 단위로 검증한다 —
FeatureTestTrait::call()은 라우트·컨트롤러·뷰가 실제로 맞물린 상태를 검사합니다. 실제 사용자가 페이지를 여는 것과 가장 가까운 방식이라, 잔손 없이 회귀(regression)를 잡기 좋습니다. - 메모리 DB로 빠르고 안전하게 — SQLite
:memory:덕분에 테스트가 실제 데이터를 건드리지 않고 순식간에 끝납니다. 뒤에서 DB가 필요한 테스트(글 목록·저장 등)를 마음 편히 늘릴 수 있습니다.
마무리 — 커밋과 태그
이번 회차에서 한 일:
- 테스트용 SQLite 메모리 DB 그룹과 PHPUnit 설정이 이미 갖춰져 있음을 확인(변경 없음)
tests/Feature/PagesTest.php추가 —/about200 응답 스모크 테스트와<h1>"소개" 렌더링 확인composer test로 초록 확인
한 번에 커밋하고 태그를 답니다.
git add .
git commit -m "test: 테스트 환경 점검과 첫 테스트"
git tag ep04
이것으로 섹션 1(스택·세팅·구조)이 마무리됩니다. 프로젝트 골격과 요청 흐름, 공통 레이아웃과 디자인, 그리고 테스트 안전망까지 다 깔았습니다.
다음 회차
다음 글 부터는 섹션 2 "글 읽기의 기초"로 들어갑니다. 첫 순서는 posts 마이그레이션 — CI4의 Forge로 글을 담을 posts 테이블(제목·slug·본문·작성자·타임스탬프)의 스키마를 정의하고 php spark migrate로 실제 테이블을 만듭니다. 드디어 데이터베이스가 등장합니다.
이번 회차 요약
- 다루는 파일:
tests/Feature/PagesTest.php(추가).app/Config/Database.php의tests그룹과 PHPUnit 설정은 기본값이 이미 충족하여 변경 없음.- 핵심: Feature 테스트는
FeatureTestTrait::call()로 실제 라우트를 호출해 컨트롤러 → 뷰 흐름을 검증한다. 테스트 DB는 SQLite:memory:라 안전하고 빠르다.assertStatus(200)으로 생존을,assertSee('소개', 'h1')으로 렌더링을 확인한다.
다음: posts 마이그레이션
댓글 0
아직 댓글이 없습니다.