CLI Overview
Quick Start Guide
wheels info
wheels reload
wheels deps
wheels destroy
wheels watch
wheels generate app
wheels generate app-wizard
wheels generate controller
wheels generate model
wheels generate view
wheels generate property
wheels generate route
wheels generate resource
wheels generate api-resource
wheels generate frontend
wheels generate test
wheels generate snippets
wheels scaffold
wheels db create
wheels db drop
wheels db setup
wheels db reset
wheels db status
wheels db version
wheels db rollback
wheels db seed
wheels db dump
wheels db restore
wheels db shell
wheels db schema
wheels dbmigrate info
wheels dbmigrate latest
wheels dbmigrate up
wheels dbmigrate down
wheels dbmigrate reset
wheels dbmigrate exec
wheels dbmigrate create blank
wheels dbmigrate create table
wheels dbmigrate create column
wheels dbmigrate remove table
wheels test
wheels test run
wheels test coverage
wheels test debug
wheels config list
wheels config set
wheels config env
wheels env
wheels env setup
wheels env list
wheels env switch
wheels environment
wheels console
wheels runner
wheels server
wheels server start
wheels server stop
wheels server restart
wheels server status
wheels server log
wheels server open
wheels plugins
wheels plugins list
wheels plugins install
wheels plugins remove
wheels analyze
wheels analyze code
wheels analyze performance
wheels analyze security
wheels security
wheels security scan
wheels optimize
wheels optimize performance
wheels docs
wheels docs generate
wheels docs serve
wheels ci init
wheels docker init
wheels docker deploy
wheels deploy
wheels deploy audit
wheels deploy exec
wheels deploy hooks
wheels deploy init
wheels deploy lock
wheels deploy logs
wheels deploy proxy
wheels deploy push
wheels deploy rollback
wheels deploy secrets
wheels deploy setup
wheels deploy status
wheels deploy stop
Configuration Management
Creating Commands
Service Architecture
Migrations Guide
Testing Guide
Object Relational Mapping
Creating Records
Reading Records
Updating Records
Deleting Records
Column Statistics
Dynamic Finders
Getting Paginated Data
Associations
Nested Properties
Object Validation
Object Callbacks
Calculated Properties
Transactions
Dirty Records
Soft Delete
Automatic Time Stamps
Using Multiple Data Sources
Testing Guide
Comprehensive guide to testing in Wheels applications using the CLI.
Overview
Wheels CLI provides robust testing capabilities through TestBox integration, offering:
- Unit and integration testing
- BDD-style test writing
- Watch mode for continuous testing
- Code coverage reporting
- Parallel test execution
- Docker-based testing across multiple engines and databases
Test Structure
Directory Layout
/tests/
├── Application.cfc # Test suite configuration
├── specs/ # Test specifications (alternative to folders below)
├── unit/ # Unit tests
│ ├── models/ # Model tests
│ ├── controllers/ # Controller tests
│ └── services/ # Service tests
├── integration/ # Integration tests
├── fixtures/ # Test data files
└── helpers/ # Test utilities
Test File Naming
Follow these conventions:
- Model tests:
UserTest.cfc
orUserSpec.cfc
- Controller tests:
UsersControllerTest.cfc
- Integration tests:
UserFlowTest.cfc
Writing Tests
Basic Test Structure
// tests/unit/models/UserTest.cfc
component extends="testbox.system.BaseSpec" {
function run() {
describe("User Model", function() {
beforeEach(function() {
// Setup before each test
variables.user = model("User").new();
});
afterEach(function() {
// Cleanup after each test
});
it("validates email presence", function() {
variables.user.email = "";
expect(variables.user.valid()).toBeFalse();
expect(variables.user.errors).toHaveKey("email");
});
it("validates email format", function() {
variables.user.email = "invalid-email";
expect(variables.user.valid()).toBeFalse();
expect(variables.user.errors.email).toInclude("valid email");
});
});
}
}
Model Testing
component extends="testbox.system.BaseSpec" {
function run() {
describe("Product Model", function() {
describe("Validations", function() {
it("requires a name", function() {
var product = model("Product").new();
expect(product.valid()).toBeFalse();
expect(product.errors).toHaveKey("name");
});
it("requires price to be positive", function() {
var product = model("Product").new(
name = "Test Product",
price = -10
);
expect(product.valid()).toBeFalse();
expect(product.errors.price).toInclude("greater than 0");
});
});
describe("Associations", function() {
it("has many reviews", function() {
var product = model("Product").findOne();
expect(product).toHaveKey("reviews");
expect(product.reviews()).toBeQuery();
});
});
describe("Scopes", function() {
it("filters active products", function() {
// Create test data
model("Product").create(name="Active", active=true);
model("Product").create(name="Inactive", active=false);
var activeProducts = model("Product").active().findAll();
expect(activeProducts.recordCount).toBe(1);
expect(activeProducts.name).toBe("Active");
});
});
});
}
}
Controller Testing
component extends="testbox.system.BaseSpec" {
function beforeAll() {
// Setup test request context
variables.mockController = prepareMock(createObject("component", "controllers.Products"));
}
function run() {
describe("Products Controller", function() {
describe("index action", function() {
it("returns all products", function() {
// Setup
var products = queryNew("id,name", "integer,varchar", [
[1, "Product 1"],
[2, "Product 2"]
]);
mockController.$("model").$args("Product").$returns(
mockModel.$("findAll").$returns(products)
);
// Execute
mockController.index();
// Assert
expect(mockController.products).toBe(products);
expect(mockController.products.recordCount).toBe(2);
});
});
describe("create action", function() {
it("creates product with valid data", function() {
// Setup params
mockController.params = {
product: {
name: "New Product",
price: 99.99
}
};
// Mock successful save
var mockProduct = createEmptyMock("models.Product");
mockProduct.$("save").$returns(true);
mockProduct.$("id", 123);
mockController.$("model").$args("Product").$returns(
createMock("models.Product").$("new").$returns(mockProduct)
);
// Execute
mockController.create();
// Assert
expect(mockController.flashMessages.success).toInclude("created successfully");
expect(mockController.redirectTo.action).toBe("show");
expect(mockController.redirectTo.key).toBe(123);
});
});
});
}
}
Integration Testing
component extends="testbox.system.BaseSpec" {
function run() {
describe("User Registration Flow", function() {
it("allows new user to register", function() {
// Visit registration page
var event = execute(event="users.new", renderResults=true);
expect(event.getRenderedContent()).toInclude("Register");
// Submit registration form
var event = execute(
event = "users.create",
eventArguments = {
user: {
email: "test@example.com",
password: "SecurePass123!",
passwordConfirmation: "SecurePass123!"
}
}
);
// Verify user created
var user = model("User").findOne(where="email='test@example.com'");
expect(user).toBeObject();
// Verify logged in
expect(session.userId).toBe(user.id);
// Verify redirect
expect(event.getValue("relocate_URI")).toBe("/dashboard");
});
});
}
}
Test Helpers
Creating Test Factories
// tests/helpers/Factories.cfc
component {
function createUser(struct overrides = {}) {
var defaults = {
email: "user#createUUID()#@test.com",
password: "password123",
firstName: "Test",
lastName: "User"
};
defaults.append(arguments.overrides);
return model("User").create(defaults);
}
function createProduct(struct overrides = {}) {
var defaults = {
name: "Product #createUUID()#",
price: randRange(10, 100),
stock: randRange(0, 50)
};
defaults.append(arguments.overrides);
return model("Product").create(defaults);
}
}
Test Data Management
// tests/helpers/TestDatabase.cfc
component {
function setUp() {
// Start transaction
transaction action="begin";
}
function tearDown() {
// Rollback transaction
transaction action="rollback";
}
function clean() {
// Clean specific tables
queryExecute("DELETE FROM users WHERE email LIKE '%@test.com'");
queryExecute("DELETE FROM products WHERE name LIKE 'Test%'");
}
function loadFixtures(required string name) {
var fixtures = deserializeJSON(
fileRead("/tests/fixtures/#arguments.name#.json")
);
for (var record in fixtures) {
queryExecute(
"INSERT INTO #arguments.name# (#structKeyList(record)#)
VALUES (#structKeyList(record, ':')#)",
record
);
}
}
}
Running Tests
Basic Commands
# Run all tests
wheels test run
# Run specific test file
wheels test run tests/unit/models/UserTest.cfc
# Run tests in directory
wheels test run tests/unit/models/
# Run with specific reporter
wheels test run --reporter=json
wheels test run --reporter=junit --outputFile=results.xml
Watch Mode
# Watch for changes and rerun tests
wheels test run --watch
# Watch specific directory
wheels test run tests/models --watch
# Watch with custom debounce
wheels test run --watch --watchDelay=1000
Filtering Tests
# Run by test bundles
wheels test run --bundles=models,controllers
# Run by labels
wheels test run --labels=critical
# Run by test name pattern
wheels test run --filter="user"
# Exclude patterns
wheels test run --excludes="slow,integration"
Code Coverage
Generate Coverage Report
# Generate HTML coverage report
wheels test coverage
# With custom output directory
wheels test coverage --outputDir=coverage-reports
# Include only specific paths
wheels test coverage --includes="models/,controllers/"
Coverage Configuration
In tests/Application.cfc
:
this.coverage = {
enabled: true,
includes: ["models", "controllers"],
excludes: ["tests", "wheels"],
outputDir: expandPath("/tests/coverage/"),
reportFormats: ["html", "json"]
};
Test Configuration
Test Suite Configuration
// tests/Application.cfc
component {
this.name = "WheelsTestSuite" & Hash(GetCurrentTemplatePath());
// Test datasource
this.datasources["test"] = {
url: "jdbc:h2:mem:test;MODE=MySQL",
driver: "org.h2.Driver"
};
this.datasource = "test";
// TestBox settings
this.testbox = {
bundles: ["tests"],
recurse: true,
reporter: "simple",
reportpath: "/tests/results",
runner: ["tests/runner.cfm"],
labels: [],
options: {}
};
}
Environment Variables
# Set test environment
export WHEELS_ENV=testing
# Set test datasource
export WHEELS_TEST_DATASOURCE=myapp_test
# Enable verbose output
export TESTBOX_VERBOSE=true
Testing Best Practices
1. Test Organization
tests/
├── unit/ # Fast, isolated tests
│ ├── models/ # One file per model
│ └── services/ # Service layer tests
├── integration/ # Tests with dependencies
└── e2e/ # End-to-end tests
2. Test Isolation
describe("User Model", function() {
beforeEach(function() {
// Fresh instance for each test
variables.user = model("User").new();
// Clear caches
application.wheels.cache.queries = {};
});
afterEach(function() {
// Clean up test data
if (isDefined("variables.user.id")) {
variables.user.delete();
}
});
});
3. Descriptive Tests
// Good: Descriptive test names
it("validates email format with standard RFC 5322 regex", function() {
// test implementation
});
it("prevents duplicate email addresses case-insensitively", function() {
// test implementation
});
// Bad: Vague test names
it("works", function() {
// test implementation
});
4. AAA Pattern
it("calculates order total with tax", function() {
// Arrange
var order = createOrder();
var item1 = createOrderItem(price: 100, quantity: 2);
var item2 = createOrderItem(price: 50, quantity: 1);
order.addItem(item1);
order.addItem(item2);
// Act
var total = order.calculateTotal(taxRate: 0.08);
// Assert
expect(total).toBe(270); // (200 + 50) * 1.08
});
Continuous Integration
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup CommandBox
uses: Ortus-Solutions/setup-commandbox@v2.0.0
- name: Install dependencies
run: box install
- name: Run tests
run: |
box server start
wheels test run --reporter=junit --outputFile=test-results.xml
- name: Upload test results
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results.xml
- name: Generate coverage
run: wheels test coverage
- name: Upload coverage
uses: codecov/codecov-action@v1
Pre-commit Hooks
#!/bin/bash
# .git/hooks/pre-commit
echo "Running tests..."
wheels test run --labels=unit
if [ $? -ne 0 ]; then
echo "Tests failed! Commit aborted."
exit 1
fi
echo "Running linter..."
wheels analyze code
if [ $? -ne 0 ]; then
echo "Code quality check failed!"
exit 1
fi
Debugging Tests
Using Debug Output
it("processes data correctly", function() {
var result = processData(testData);
// Debug output
debug(result);
writeDump(var=result, abort=false);
// Conditional debugging
if (request.debug ?: false) {
writeOutput("Result: #serializeJSON(result)#");
}
expect(result.status).toBe("success");
});
Interactive Debugging
# Run specific test with debugging
wheels test debug tests/unit/models/UserTest.cfc
# Enable verbose mode
wheels test run --verbose
# Show SQL queries
wheels test run --showSQL
Performance Testing
Load Testing
describe("Performance", function() {
it("handles 1000 concurrent users", function() {
var threads = [];
for (var i = 1; i <= 1000; i++) {
arrayAppend(threads, function() {
var result = model("Product").findAll();
return result.recordCount;
});
}
var start = getTickCount();
var results = parallel(threads);
var duration = getTickCount() - start;
expect(duration).toBeLT(5000); // Less than 5 seconds
expect(arrayLen(results)).toBe(1000);
});
});
Common Testing Patterns
Testing Private Methods
it("tests private method", function() {
var user = model("User").new();
// Use makePublic() for testing
makePublic(user, "privateMethod");
var result = user.privateMethod();
expect(result).toBe("expected");
});
Mocking External Services
it("sends email on user creation", function() {
// Mock email service
var mockMailer = createEmptyMock("services.Mailer");
mockMailer.$("send").$returns(true);
// Inject mock
var user = model("User").new();
user.$property("mailer", mockMailer);
// Test
user.save();
// Verify
expect(mockMailer.$times("send")).toBe(1);
expect(mockMailer.$callLog().send[1].to).toBe(user.email);
});
Docker-Based Testing
Wheels provides a comprehensive Docker environment for testing across multiple CFML engines and databases.
Quick Start with Docker
# Start the TestUI and all test containers
docker compose --profile all up -d
# Access the TestUI
open http://localhost:3000
TestUI Features
The modern TestUI provides:
- Visual Test Runner: Run and monitor tests in real-time
- Container Management: Start/stop containers directly from the UI
- Multi-Engine Support: Test on Lucee 5/6 and Adobe ColdFusion 2018/2021/2023
- Multi-Database Support: MySQL, PostgreSQL, SQL Server, H2, and Oracle
- Pre-flight Checks: Ensures all services are running before tests
- Test History: Track test results over time
Container Management
The TestUI includes an API server that allows you to:
- Click on any stopped engine or database to start it
- Monitor container health and status
- View real-time logs
- No terminal required for basic operations
Docker Profiles
Use profiles to start specific combinations:
# Just the UI
docker compose --profile ui up -d
# Quick test setup (Lucee 5 + MySQL)
docker compose --profile quick-test up -d
# All Lucee engines
docker compose --profile lucee up -d
# All Adobe engines
docker compose --profile adobe up -d
# All databases
docker compose --profile db up -d
Running Tests via Docker
# Using the CLI inside a container
docker exec -it wheels-lucee5-1 wheels test run
# Direct URL access
curl http://localhost:60005/wheels/testbox?format=json&db=mysql
Database Testing
Test against different databases by using the db
parameter:
# MySQL
wheels test run --db=mysql
# PostgreSQL
wheels test run --db=postgres
# SQL Server
wheels test run --db=sqlserver
# H2 (Lucee only)
wheels test run --db=h2
# Oracle
wheels test run --db=oracle
See Also
- wheels test run - Test execution command
- wheels test coverage - Coverage generation
- wheels generate test - Generate test files
- TestBox Documentation - Complete TestBox guide
- Docker Testing Guide - Detailed Docker testing documentation
- Overview
- Test Structure
- Directory Layout
- Test File Naming
- Writing Tests
- Basic Test Structure
- Model Testing
- Controller Testing
- Integration Testing
- Test Helpers
- Creating Test Factories
- Test Data Management
- Running Tests
- Basic Commands
- Watch Mode
- Filtering Tests
- Code Coverage
- Generate Coverage Report
- Coverage Configuration
- Test Configuration
- Test Suite Configuration
- Environment Variables
- Testing Best Practices
- 1. Test Organization
- 2. Test Isolation
- 3. Descriptive Tests
- 4. AAA Pattern
- Continuous Integration
- GitHub Actions
- Pre-commit Hooks
- Debugging Tests
- Using Debug Output
- Interactive Debugging
- Performance Testing
- Load Testing
- Common Testing Patterns
- Testing Private Methods
- Mocking External Services
- Docker-Based Testing
- Quick Start with Docker
- TestUI Features
- Container Management
- Docker Profiles
- Running Tests via Docker
- Database Testing
- See Also