Command Line Tools
Testing Guide
Testing Guide
Section titled “Testing Guide”Comprehensive guide to testing in Wheels applications using the CLI.
Overview
Section titled “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
Section titled “Test Structure”Directory Layout
Section titled “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 utilitiesTest File Naming
Section titled “Test File Naming”Follow these conventions:
- Model tests:
UserTest.cfcorUserSpec.cfc - Controller tests:
UsersControllerTest.cfc - Integration tests:
UserFlowTest.cfc
Writing Tests
Section titled “Writing Tests”Basic Test Structure
Section titled “Basic Test Structure”component extends="wheels.Testbox" {
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
Section titled “Model Testing”component extends="wheels.Testbox" {
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
Section titled “Controller Testing”component extends="wheels.Testbox" {
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
Section titled “Integration Testing”component extends="wheels.Testbox" {
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
Section titled “Test Helpers”Creating Test Factories
Section titled “Creating Test Factories”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
Section titled “Test Data Management”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
Section titled “Running Tests”Basic Commands
Section titled “Basic Commands”# Run all testswheels test run
# Run specific test filewheels test run tests/unit/models/UserTest.cfc
# Run tests in directorywheels test run tests/unit/models/
# Run with specific reporterwheels test run --reporter=jsonwheels test run --reporter=junit --outputFile=results.xmlWatch Mode
Section titled “Watch Mode”# Watch for changes and rerun testswheels test run --watch
# Watch specific directorywheels test run tests/models --watch
# Watch with custom debouncewheels test run --watch --watchDelay=1000Filtering Tests
Section titled “Filtering Tests”# Run by test bundleswheels test run --bundles=models,controllers
# Run by labelswheels test run --labels=critical
# Run by test name patternwheels test run --filter="user"
# Exclude patternswheels test run --excludes="slow,integration"Code Coverage
Section titled “Code Coverage”Generate Coverage Report
Section titled “Generate Coverage Report”# Generate HTML coverage reportwheels test coverage
# With custom output directorywheels test coverage --outputDir=coverage-reports
# Include only specific pathswheels test coverage --includes="models/,controllers/"Coverage Configuration
Section titled “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
Section titled “Test Configuration”Test Suite Configuration
Section titled “Test Suite Configuration”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
Section titled “Environment Variables”# Set test environmentexport WHEELS_ENV=testing
# Set test datasourceexport WHEELS_TEST_DATASOURCE=myapp_test
# Enable verbose outputexport TESTBOX_VERBOSE=trueTesting Best Practices
Section titled “Testing Best Practices”1. Test Organization
Section titled “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 tests2. Test Isolation
Section titled “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
Section titled “3. Descriptive Tests”// Good: Descriptive test namesit("validates email format with standard RFC 5322 regex", function() { // test implementation});
it("prevents duplicate email addresses case-insensitively", function() { // test implementation});
// Bad: Vague test namesit("works", function() { // test implementation});4. AAA Pattern
Section titled “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
Section titled “Continuous Integration”GitHub Actions
Section titled “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@v1Pre-commit Hooks
Section titled “Pre-commit Hooks”#!/bin/bashecho "Running tests..."wheels test run --labels=unit
if [ $? -ne 0 ]; then echo "Tests failed! Commit aborted." exit 1fi
echo "Running linter..."wheels analyze code
if [ $? -ne 0 ]; then echo "Code quality check failed!" exit 1fiDebugging Tests
Section titled “Debugging Tests”Using Debug Output
Section titled “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
Section titled “Interactive Debugging”# Run specific test with debuggingwheels test debug tests/unit/models/UserTest.cfc
# Enable verbose modewheels test run --verbose
# Show SQL querieswheels test run --showSQLPerformance Testing
Section titled “Performance Testing”Load Testing
Section titled “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
Section titled “Common Testing Patterns”Testing Private Methods
Section titled “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
Section titled “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
Section titled “Docker-Based Testing”Wheels provides a comprehensive Docker environment for testing across multiple CFML engines and databases.
Quick Start with Docker
Section titled “Quick Start with Docker”# Start the TestUI and all test containersdocker compose --profile all up -d
# Access the TestUIopen http://localhost:3000TestUI Features
Section titled “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
Section titled “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
Section titled “Docker Profiles”Use profiles to start specific combinations:
# Just the UIdocker compose --profile ui up -d
# Quick test setup (Lucee 5 + MySQL)docker compose --profile quick-test up -d
# All Lucee enginesdocker compose --profile lucee up -d
# All Adobe enginesdocker compose --profile adobe up -d
# All databasesdocker compose --profile db up -dRunning Tests via Docker
Section titled “Running Tests via Docker”# Using the CLI inside a containerdocker exec -it wheels-lucee5-1 wheels test run
# Direct URL accesscurl http://localhost:60005/wheels/core/tests?format=json&db=mysqlDatabase Testing
Section titled “Database Testing”Test against different databases by using the db parameter:
# MySQLwheels test run --db=mysql
# PostgreSQLwheels test run --db=postgres
# SQL Serverwheels test run --db=sqlserver
# H2 (Lucee only)wheels test run --db=h2
# Oraclewheels test run --db=oracleSee Also
Section titled “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