Lesson  in  ColdFusion 2025: Foundations

Testing & Debugging CFML

Debug CFML applications using cfdump, cflog, and the CF Admin debugger. Write unit tests with TestBox and run them via CommandBox against the live TicketService already running in your lab.

Debugging and testing — two tools every CF developer needs

Writing code that works once in a happy path is easy. Writing code that keeps working as requirements change, that fails clearly when something is wrong, and that you can fix quickly when it breaks — that requires two disciplines: debugging and testing.

Debugging is how you understand what your code is actually doing right now. ColdFusion gives you cfdump, cflog, and the CF Admin debugger — tools that let you inspect any variable, trace execution, and read structured log entries without touching production.

Testing is how you prove your code does what it should — and keep proving it every time you change something. ColdFusion's testing ecosystem is built around TestBox, the de-facto standard BDD/TDD framework for CFML, built by Ortus Solutions and documented at testbox.ortusbooks.com.

In this lesson you will:

  1. Use cfdump to inspect live data structures in the browser
  2. Write structured log entries with cflog and read them from the terminal
  3. Install TestBox and write a test spec against the TicketService already running in your lab
  4. Run the test suite and verify it passes

Who does this? Debugging tools are used by every developer daily. Unit testing is a developer responsibility — not something reserved for QA. In a well-run team, tests live alongside the code and run on every commit.


1. Debugging with cfdump

cfdump renders any ColdFusion variable as a colour-coded HTML table — the fastest way to inspect data during development.

<cfset data = {name: "Alex", scores: [95, 87, 72]}>
<cfdump var="#data#" label="Student data">
Example cfdump HTML output — a colour-coded nested table labelled Student data at the top; outer row shows type struct with key name value Alex type string and key scores type array; the array expands to show three numeric cells 95 87 72 — styled with the classic ColdFusion blue header bar and alternating white and grey rows

cfdump renders any CF variable as a colour-coded interactive table — the fastest debugging tool in CFML.

Useful cfdump attributes:

AttributePurpose
varVariable to dump (required)
labelHeading above the dump
topLimit depth of nested structures — prevents huge dumps
expandtrue (default) expands nested structures; false collapses them
output"browser" (default) renders HTML; "console" writes to the CF console log
format"html" (default) or "text" — use "text" for CLI output
💡 Never leave cfdump in production code

cfdump outputs HTML directly into the response. If a cfdump call is left in a JSON API endpoint it will corrupt the JSON and break API consumers. Use it only during development, always remove it before committing. A quick check before deploy:

grep -r "cfdump" /opt/coldfusion2025/cfusion/wwwroot/

If that returns any hits, remove them before shipping.


2. Logging with cflog

cflog writes structured entries to a named log file in CF's log directory — visible in the terminal without touching the browser.

<cflog file="training" text="Processing ticket #url.id#"    type="information">
<cflog file="training" text="Ticket not found: #url.id#"    type="warning">
<cflog file="training" text="DB error: #cfcatch.message#"   type="error">
<cflog file="training" text="Server shutting down"          type="fatal">

cflog attributes:

AttributeRequiredDescription
fileYesLog filename — CF creates <name>.log in the CF logs directory automatically
textYesMessage to write — can include any CFML expressions
typeNoSeverity: information, warning, error, fatal — default is information
applicationNotrue adds the application name to the entry; false (default) omits it
threadNotrue adds the thread ID — useful for debugging async/scheduled tasks

The file attribute sets the log filename — CF creates the file automatically on the first write. It will not exist until at least one cflog call has executed. Once it does, you can tail it live:

tail -f /opt/coldfusion2025/cfusion/logs/training.log

The log format includes a timestamp, thread ID, severity, and your message — structured and grep-friendly.

⚠️ "No such file or directory" — why tail fails before the first write

If you run tail -f before any cflog call has executed, you will see:

tail: cannot open '.../training.log' for reading: No such file or directory
tail: no files remaining

This is expected — ColdFusion does not create the log file until the first entry is written. The correct sequence is:

Step 1 — add a cflog call to a page (skip if you already have one):

sudo tee -a /opt/coldfusion2025/cfusion/wwwroot/debug-demo.cfm << 'EOF'
<cflog file="training" text="test entry from debug-demo.cfm" type="information">
EOF

Step 2 — trigger the first write:

curl -s http://localhost:8500/debug-demo.cfm > /dev/null

Step 3 — now tail the file:

tail -f /opt/coldfusion2025/cfusion/logs/training.log

If you want to open the tail first and wait, use -F instead of -f — it retries until the file appears:

tail -F /opt/coldfusion2025/cfusion/logs/training.log
💡 cflog vs cfdump — when to use which
  • cfdump — use during active development when you want to see a variable's full structure immediately in the browser. Remove before committing.
  • cflog — use for persistent, structured audit trails: recording who did what, catching errors in background tasks, tracing slow queries. Leave it in production code — it writes to a file, not the response.

3. TestBox — unit testing for CFML

TestBox is the de-facto standard testing framework for CFML, built and maintained by Ortus Solutions. It is open source, actively maintained, and the framework you will encounter on almost every serious CFML project.

Full documentation: testbox.ortusbooks.com

What is TestBox — features, versions, and how it fits with ColdFusion

TestBox ships with everything a CFML developer needs to write and run tests:

FeatureWhat it gives you
BDD syntaxdescribe, it, expect blocks that read like plain English specifications
TDD syntaxTraditional @Test annotation style for developers who prefer it
Mocking enginecreateMock() and createStub() isolate a CFC from its dependencies
Multiple runnersStreamingRunner.cfm (plain text), HTMLRunner.cfm (browser), TextRunner.cfm (CI-friendly)
Rich reportersText, JSON, TAP, JUnit XML — integrates with GitHub Actions, Jenkins, GitLab CI
beforeEach / afterEachPer-test setup and teardown — reset state between each it() block

How it installs: TestBox is a CommandBox package — it installs into your project directory alongside your app code, not inside ColdFusion itself. The same test suite runs against Adobe CF, Lucee, or any CFML engine without changes.

Versions: The current stable release is TestBox 6.x, which requires CommandBox 6+ and Java 11+. The lab VM ships with both.

TestBox vs ColdFusion: TestBox is not part of Adobe ColdFusion — it is a community package. Adobe CF ships with MXUnit (an older built-in test runner), but the CFML community has moved to TestBox as the standard. When you search for CFML testing examples online, TestBox is what you will find.

Further reading:

💡 How TestBox fits into this lab — two servers, two roles

The lab runs two separate servers. It is important to understand which server does what:

ServerPortRuntimeRole
ColdFusion 20258500Adobe CFRuns your CF application (/opt/coldfusion2025/cfusion/wwwroot/)
CommandBox / Lucee8888LuceeRuns ~/app/ — including TestBox and the TicketService being tested

TestBox itself runs on port 8888 (Lucee/CommandBox). Your test specs live in ~/app/tests/ and are served by that Lucee server. The TicketService.cfc and seed-db.cfm files in ~/app/ are what the specs test — they are not the same files in the CF wwwroot.

When you run:

curl "http://localhost:8888/testbox/system/runners/StreamingRunner.cfm?directory=tests"

You are asking the Lucee server to execute your specs. Lucee loads your TicketService.cfc from ~/app/, queries the in-memory H2 database declared in ~/app/Application.cfc, and reports the results.

Annotated CFML code snippet of a TestBox BDD spec — the component extends testbox.system.BaseSpec line is labelled Extends BaseSpec; the describe TicketService block is labelled Test suite groups related tests; the it should return all tickets block is labelled Individual test case one behaviour; the expect result toBeArray line is labelled Assertion checks the result — each label is connected to its code line by a dashed callout line

TestBox BDD structure: describe groups related tests, it defines one behaviour, expect asserts the outcome.

Install TestBox

TestBox installs into your student app directory via CommandBox:

cd ~/app
box install testbox

This downloads TestBox into ~/app/testbox/ and makes the test runner available at http://localhost:8888/testbox/.

[object Object]

box install testbox pulls TestBox and its dependencies from ForgeBox. The "Initializing libraries" message only appears on the first run — subsequent installs are faster.

What TicketService does

Before writing the spec, understand what you are testing. TicketService.cfc in ~/app/ is a service CFC that queries the hd_tickets table in the Lucee-side H2 database. It exposes two methods:

MethodWhat it returns
getAll()An array of ticket structs — every row in hd_tickets
getById(id)A single ticket struct for the given primary key

The beforeAll block in the test spec calls seed-db.cfm via cfhttp to populate the in-memory H2 database before any test runs. Without that seed call, getAll() returns an empty array and getById(1) returns nothing — both tests would fail.

# Verify TicketService and seed-db.cfm exist in ~/app/
ls ~/app/TicketService.cfc ~/app/seed-db.cfm

Write a test spec

Create the tests directory and spec file:

mkdir -p ~/app/tests
tee ~/app/tests/TicketServiceTest.cfc << 'EOF'
component extends="testbox.system.BaseSpec" {

  function run() {

    describe("TicketService", function() {

      // Seed the database once before the tests are defined.
      // This TestBox version only has beforeEach/afterEach — no beforeAll.
      // Calling cfhttp here runs once when the describe closure is executed.
      cfhttp(url="http://localhost:8888/seed-db.cfm", method="GET");

      var svc = new TicketService();

      it("should return all tickets as an array", function() {
        var result = svc.getAll();
        expect(result).toBeArray();
        expect(arrayLen(result)).toBeGTE(1);
      });

      it("should return a single ticket by id", function() {
        var ticket = svc.getById(1);
        expect(ticket).toBeStruct();
        expect(ticket).toHaveKey("title");
      });

    });

  }
}
EOF
Annotated TestBox BDD spec file — the component declaration, run function, describe block, it block, and expect assertions are each labelled with callout lines explaining their role in the spec structure

Anatomy of a TestBox spec: each part has a specific role — understand the structure before running the suite.

What each line does:

PartWhat it does
extends="testbox.system.BaseSpec"Required — gives the CFC all TestBox assertion and runner methods
function run()TestBox calls this automatically to discover and run all specs
describe("TicketService", ...)Groups related tests — the name appears in failure messages
cfhttp(url="...seed-db.cfm")Runs once when the describe closure executes — seeds the DB before any it()
var svc = new TicketService()Creates a fresh instance of the service — available to all it() blocks below
it("should ...", function() {...})One test case — describes one expected behaviour
expect(result).toBeArray()Asserts the result is a CF array — throws if it is not
expect(arrayLen(result)).toBeGTE(1)Asserts at least one record exists — requires the seed to have run
💡 Common TestBox matchers — quick reference
MatcherWhat it checks
toBeArray()Value is a CF array
toBeStruct()Value is a CF struct
toBeTrue() / toBeFalse()Boolean result
toBe(value)Strict equality (==)
toBeNull()Value is null
toHaveKey("key")Struct contains the named key
toBeGTE(n)Greater than or equal to n
toBeLTE(n)Less than or equal to n
toInclude("text")String or array contains value
toBeEmpty()Array, struct, or string is empty
toThrow()Wrapped function throws an exception
toThrow(type="...")Throws a specific exception type

Full matcher list: testbox.ortusbooks.com/content/matchers

💡 TestBox and every other testing framework — the same pattern

TestBox follows the same BDD/TDD patterns used in every major language:

BDD styledescribe / it / expect

FrameworkLanguage
TestBoxCFML
Jest / Vitest / JasmineJavaScript
RSpecRuby
pytest (with plugins)Python

TDD style — annotation-based

FrameworkLanguage
TestBox @TestCFML
JUnit @TestJava
NUnit / xUnit [Test] / [Fact]C#
PHPUnit @testPHP

The one CFML-specific detail: instead of a CLI command like jest or pytest, you hit a URL — because the test engine runs inside the application server. Everything else maps directly.

Run the tests

Hit the StreamingRunner with curl — plain text output, no interactive CLI, no hanging:

curl -s "http://localhost:8888/testbox/system/runners/StreamingRunner.cfm?directory=tests"

A passing suite ends with:

Tests: 2 Passed: 2 Failed: 0 Errors: 0 Skipped: 0

A failing test shows exactly which it() block failed and why:

[FAIL] TicketService > should return all tickets as an array
  Expected [] to have a length greater than or equal to 1
  >> Did you forget to run seed-db.cfm in beforeAll?
📖 TDD, BDD, and SDD — testing approaches compared

Three approaches to testing — all valid, each framing the work differently:

TDD (Test-Driven Development) — write the test first, watch it fail, then write the code to make it pass. The test drives the implementation — you cannot write code without a failing test to justify it.

BDD (Behaviour-Driven Development) — write tests that describe expected behaviour in plain language: "it should return all tickets as an array." TestBox's describe/it/expect syntax is BDD. A failing test message tells both developers and product managers exactly what stopped working.

SDD (Specification-Driven Development) — the direction the industry is moving toward with AI-assisted development. The specification is the source of truth — both code and tests are generated or verified against it.

SDD is not a replacement for TDD or BDD — it is a layer above them. The generated tests still run as TDD or BDD tests.

🎓 AI-assisted development in ColdFusion — generating CFML from specs, building pipelines where a spec drives code generation and test validation — is covered in the Advanced Course.


4. CF Admin Debugger

For deeper request-level tracing, ColdFusion's built-in debugger appends a full diagnostic panel to every rendered page — showing SQL queries executed, their execution times, template load times, and variable scopes.

Enable it in CF Admin:

  1. Open CF Admin → Debugging & Logging → Debug Output Settings
  2. Check Enable Request Debugging Output
  3. Add 127.0.0.1 to the IP address restriction list so only your terminal sees the output

Debugging output appears at the bottom of every CF page response. It is automatically hidden from any IP not on the list — safe to leave enabled in the lab, but always disable it before any public deployment.


Activity 1 — Inspect data with cfdump

Create a page that dumps a struct and array so you can see cfdump in action:

sudo tee /opt/coldfusion2025/cfusion/wwwroot/debug-demo.cfm << 'EOF'
<cfscript>
  ticket = {
    id:       1,
    title:    "Cannot connect to VPN",
    status:   "open",
    priority: "high",
    tags:     ["network", "remote", "urgent"]
  };
</cfscript>

<cfdump var="#ticket#" label="Sample ticket struct">
EOF

Open the ColdFusion browser tab and navigate to /debug-demo.cfm. You should see the colour-coded dump table with the nested tags array expanded inline.


Activity 2 — Write a log entry and read it

Add a cflog call to your debug page and tail the log file:

sudo tee -a /opt/coldfusion2025/cfusion/wwwroot/debug-demo.cfm << 'EOF'
<cflog file="training" text="debug-demo.cfm loaded by #cgi.REMOTE_ADDR#" type="information">
EOF

Now tail the log in a second terminal tab while you refresh the page:

tail -f /opt/coldfusion2025/cfusion/logs/training.log

Each page load should add a new timestamped line to the log.


Activity 3 — Install TestBox and write a test spec

First, verify the files TestBox will test are in place:

ls ~/app/TicketService.cfc ~/app/seed-db.cfm ~/app/Application.cfc

All three must exist. If any are missing, see the hint boxes below before continuing.

Install TestBox into your student app:

cd ~/app && box install testbox

Then create the spec:

mkdir -p ~/app/tests
tee ~/app/tests/TicketServiceTest.cfc << 'EOF'
component extends="testbox.system.BaseSpec" {

  function run() {

    describe("TicketService", function() {

      // Seed the database once before the tests are defined.
      // This TestBox version only has beforeEach/afterEach — no beforeAll.
      // Calling cfhttp here runs once when the describe closure is executed.
      cfhttp(url="http://localhost:8888/seed-db.cfm", method="GET");

      var svc = new TicketService();

      it("should return all tickets as an array", function() {
        var result = svc.getAll();
        expect(result).toBeArray();
        expect(arrayLen(result)).toBeGTE(1);
      });

      it("should return a single ticket by id", function() {
        var ticket = svc.getById(1);
        expect(ticket).toBeStruct();
        expect(ticket).toHaveKey("title");
      });

    });

  }
}
EOF

Activity 4 — Run the tests

curl -s "http://localhost:8888/testbox/system/runners/StreamingRunner.cfm?directory=tests"

All tests must pass — zero failures, zero errors.

⚠️ "Page StreamingRunner.cfm not found" — TestBox installed in the wrong directory

This error means Lucee cannot find ~/app/testbox/. Verify:

ls ~/app/testbox/system/runners/

You should see StreamingRunner.cfm. If the directory does not exist, re-install from the correct location:

cd ~/app && box install testbox

The cd ~/app is required — box install places packages relative to the current working directory. Installing from ~ or any other path puts testbox/ somewhere the Lucee server (rooted at ~/app/) cannot reach.

⚠️ "No matching function [BEFOREALL] found" — beforeAll does not exist in this TestBox version

The version of TestBox installed in this lab (box install testbox pulls the latest) only exposes beforeEach and afterEach as lifecycle hooks inside describe. There is no beforeAll.

Check what your installed version actually provides:

grep -n "function before\|function after\|function setUp\|function setup" ~/app/testbox/system/BaseSpec.cfc | head -20

Do not use beforeAll — it does not exist. Instead, place any one-time setup code (like the seed call) directly at the top of the describe closure body. It executes once when TestBox processes the suite:

describe("TicketService", function() {
  // Runs once when TestBox processes this describe block — no lifecycle hook needed
  cfhttp(url="http://localhost:8888/seed-db.cfm", method="GET");

  it("should ...", function() { ... });
});
⚠️ Tests fail with "Expected [] to have length GTE 1" — seed-db.cfm not running

This means the beforeAll seed call failed silently. The database is empty so getAll() returns an empty array. Debug steps:

Step 1 — test seed-db.cfm directly:

curl -s http://localhost:8888/seed-db.cfm

You should see output confirming rows were inserted. If you see an error, the seed file has a problem.

Step 2 — check the Lucee server is running:

curl -s -o /dev/null -w "%{http_code}" http://localhost:8888/

Should return 200. If not, restart: cd ~/app && box server restart

Step 3 — verify seed-db.cfm exists:

ls ~/app/seed-db.cfm
⚠️ Why curl and not box testbox run?

box testbox run starts an interactive CommandBox session that can hang indefinitely in a non-TTY terminal. Hitting the StreamingRunner URL directly with curl is equivalent — it calls the same runner, returns the same plain-text output, and never blocks.

💡 TicketService and seed-db.cfm — where they live

TicketService.cfc and seed-db.cfm are in ~/app/ — the Lucee web root. They are not the same files as in /opt/coldfusion2025/cfusion/wwwroot/. The ones in ~/app/ are what the TestBox runner uses. seed-db.cfm is called by the spec's beforeAll block via cfhttp to populate the Lucee-side in-memory H2 database before the tests run.

⚠️ "Datasource [training_db] doesn't exist" — Application.cfc missing or not loaded

TicketService.cfc queries a datasource named training_db declared in ~/app/Application.cfc. Verify it exists:

cat ~/app/Application.cfc

It should contain:

component {
  this.name = "cfTrainingApp";

  this.datasources["training_db"] = {
    class:            "org.h2.Driver",
    connectionString: "jdbc:h2:mem:training_db;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE",
    username:         "sa",
    password:         ""
  };
}

If missing, create it and restart:

tee ~/app/Application.cfc << 'EOF'
component {
  this.name = "cfTrainingApp";

  this.datasources["training_db"] = {
    class:            "org.h2.Driver",
    connectionString: "jdbc:h2:mem:training_db;DB_CLOSE_DELAY=-1;DATABASE_TO_UPPER=FALSE",
    username:         "sa",
    password:         ""
  };
}
EOF
cd ~/app && box server restart

DATABASE_TO_UPPER=FALSE tells H2 to preserve column-name case exactly as written in SQL.


Further reading


When all the checks above are green, this lesson is complete. Your progress is saved automatically — move straight on to the next lesson.