$name, 'data' => $data, 'options' => $options]; } } } namespace Tests\App\Controllers\View { use App\Controllers\View\ExpenseController; use CodeIgniter\Test\CIUnitTestCase; use Config\Services; class TestableExpenseController extends ExpenseController { public function __construct() { // Skip the real constructor so tests can inject fakes. } public function setExpenseModel(object $model): self { $this->expenseModel = $model; return $this; } public function setConfigModel(object $model): self { $this->configModel = $model; return $this; } } class ExpenseIndexFakeModel { public array $whereCalls = []; public function select(string $select): self { return $this; } public function join(string $table, string $condition, string $type = ''): self { return $this; } public function where(string $field, mixed $value): self { $this->whereCalls[] = [$field, $value]; return $this; } public function orderBy(string $field, string $direction = ''): self { return $this; } public function findAll(): array { return [ [ 'id' => 10, 'category' => 'Expense', 'amount' => '12.50', 'receipt_path' => 'receipt.pdf', 'school_year' => '2025-2026', ], ]; } } class ExpenseIndexFakeConfig { public function getConfig(string $key): ?string { return $key === 'school_year' ? '2025-2026' : null; } } class ExpenseFakeRenderer { private array $data = []; private array $lastRender = []; public function setData(?array $data = null): self { $this->data = $data ?? []; return $this; } public function render(string $view, array $data = [], $options = null) { $this->lastRender = [ 'view' => $view, 'data' => $data ?: $this->data, 'options' => $options, ]; return 'fake-rendered:' . $view; } public function getLastRender(): array { return $this->lastRender; } } class ExpenseControllerTest extends CIUnitTestCase { protected function tearDown(): void { Services::resetSingle('renderer'); parent::tearDown(); } public function testIndexScopesExpensesToActiveConfiguredSchoolYear(): void { $expenseModel = new ExpenseIndexFakeModel(); $renderer = new ExpenseFakeRenderer(); Services::injectMock('renderer', $renderer); $controller = (new TestableExpenseController()) ->setExpenseModel($expenseModel) ->setConfigModel(new ExpenseIndexFakeConfig()); $result = $controller->index(); $this->assertSame('fake-rendered:expenses/index', $result); $rendered = $renderer->getLastRender(); $this->assertSame('expenses/index', $rendered['view']); $this->assertSame('2025-2026', $rendered['data']['schoolYear']); $this->assertSame([ ['expenses.school_year', '2025-2026'], ], $expenseModel->whereCalls); $this->assertSame( site_url('receipts/receipt.pdf'), $rendered['data']['expenses'][0]['receipt_url'] ); } } }