Hi @sethfeldkamp --glad to see you’re still doing CF! Sorry for the delay in response, but I just got back from vacation. Just to clarify a bit-- at first I thought you were talking about setting up a CI/CD server’s “workflow”, but then when you said you wanted it to be “interactive” it sounded like you wanted something running locally, but I’m unclear if you were expecting to specifically interact with the terminal by supplying input via your keyboard while it was running the tests? I can’t really imagine what you may have in mind there, so you’d need to unpack that thought.
There also seems to be a chance you simply mean that if you have FooService.cfc
which has a corresponding test named FooServiceTest.cfc
, and when you modify the FooService.cfc
you are wanting to automatically run the FooServiceTest.cfc
immediately. And, if two or more files were modified, you’d want each of their respective tests to run, etc…
I’m still not sure which of those options you are hitting at, but I’ll address that last one since it’s an interesting use case regardless. The biggest issue to doing that is you would need some sort of convention that allows you to determine what exact tests specs “cover” a given CFC in your code base. While you may or may not have a folder/naming convention, that’s certainly not anything enforced by TestBox itself. So, perhaps, any modifications to /models/XXX.cfc
can be assumed that the matching unit test is in tests/specs/XXXTest.cfc
, but that’s something you’d need to decide and enforce.
As far as how you’d stick something like that together-- I am a little curious why you want to tie the formatting and the test running together unless you’re just trying to reduce the number of watchers. I don’t tend to use the formatting watchers while I code, in favor of a one-time formatting/commit in my CI/CD workflow or a pre-commit hook FWIW. The watch
command is very powerful and generic, but I think it would be SUPER difficult to make it work for your purposes just due to readability. The default example of the watch
command runs a command for every file that was changed, but it would really be more efficient to do a single pass of the formatting and the test runners on all the files at once instead of running 50 testbox runs after a find/replace modifies 50 files at once. Not saying it wouldn’t work the other way, it just seems a little heavy handed.
What would probably be much much easier to write and manage would be to whip up a Task Runner that does this, where you can enforce your test naming conventions, aggregate the file changed lists, and manage the watcher. You can still even wrap up the task runner in a package script if you want the easy run-script xyx
shortcut to starting it.
So, if you’re wondering if this has been done before, the answer is probably not. While I’ve heard of this, I’ve never actually seen anyone who designed their tests names in a way where they could effectively figure out which tests to run to test a given CFC model. In our Ortus projects, we tend to rely heavily on integration testing over unit testing, which is even harder since there’s an ambiguous many-to-many relationship between which integration tests may hit a given model or models.
I just threw some code in a test Task Runner and this is what I came up with. To test this, I ran these commands:
coldbox create app
coldbox create model myService
task create --open
and then placed the following code in my task.cfc
:
component {
function run(){
watch()
.paths( "**.cfc" )
// Add excludes here as neccessary
.excludePaths( "/coldbox/", "/testbox/", "/tests/", "/task.cfc" )
.onChange( ( files ) => {
print
.line()
.line( '-------------------------------------------------------------------------' )
.line()
.toConsole();
// We care about new or changed files
var fileChangedList = files.added.append( files.changed, true ).toList();
// Format the files. The watcher is smart enough to not run recursivley here.
command( "cfformat run" )
.params( fileChangedList & "," ) // The trailing comma is just a trick to make the "cfformat run" command output a list of formatted file
.flags( "overwrite" )
.run( echo = true ) // Turn off echo to reduce debug output
print
.line()
.line()
.toConsole();
// This is where your logic goes to map modified files in your app to related test bundles
var testBundleList = fileChangedList
.listMap( ( file ) => {
// handlers/Main.cfc maps to tests.specs.integration.MainSpec. Modify as desired.
if ( file contains "handlers" ) {
return "tests.specs.integration.#file.reReplace( "handlers[\\/](.*)\.cfc", "\1" )#Spec";
// mdoels/MyService.cfc maps to tests.specs.unit.MyServiceTest. Modify as desired.
} else if ( file contains "models" ) {
return "tests.specs.unit.#file.reReplace( "models[\\/](.*)\.cfc", "\1" )#Test";
} else {
// Unmapped files are ignored
return "";
}
} )
// Ignore empty strings from the map() above
.listFilter( ( file ) => file.len() );
command( "testbox run" )
.params( bundles = testBundleList )
.flags( "noVerbose" )
.run( echo = true ) // Turn off echo to reduce debug output
} )
.start();
}
}
You can run it with
task run
or wrap it up in a package script like so:
package set scripts.watchFormatTest='task run'
than you can run like so:
run-script watchFormatTest
That should get you started and you can modify the task runner as you see fit, which gives you way more power and readability over trying to cram all that logic in a one-liner to the watch
command.