Showing posts with label Code Examples. Show all posts
Showing posts with label Code Examples. Show all posts

Thursday, 2 February 2023

PHP 8: a quick look at enums

G'day:

Whilst working on my recent code implemneting a postcode look-up web service, I stumbled across Monolog/Level:

enum Level: int
{
    // …
}

I was unaware of PHP having enums, and it turns out it's a recent addition: arriving in PHP 8.1 (I had moved on from PHP when 7.2 was brand new). See PHP: Enumerations for the docs.

My usage of it was I wanted to get the string "CRITICAL" back from the log level I was testing a log entry for:

public function assertLogEntryIsCorrect(
    TestHandler $testHandler,
    Level $expectedLogLevel,
    int $statusCode,
    string $expectedMessage
): void {
    $logRecords = $testHandler->getRecords();
    $this->assertCount(1, $logRecords);
    $this->assertEquals($expectedLogLevel->getName(), $logRecords[0]["level_name"]);

Monolog/Logger has this:

public const CRITICAL = 500;

But nothing associating that const to the string "CRITICAL", yet in the log entry the level is "CRITICAL". Yeah I could have just used strtoupper, but I had suspected there was a better way, so had a look at how Monolog actually writes the log entry. I traced this back to here:

public static function getLevelName(int|Level $level): string
{
    return self::toMonologLevel($level)->getName();
}

And from there found the Level enum

I wanted to see what enums in PHP can do, so I worked through the docs, and wrote myself an example of each. I did this via TDD, so I have some tests describing the functionality (see the @testdox annotations for the descriptions):

namespace adamcameron\php8\tests\Unit\System;

use adamcameron\php8\tests\Unit\System\Fixtures\MaoriNumbers as MI;
use PHPUnit\Framework\TestCase;

/** @testdox Testing enums */
class EnumTest extends TestCase
{

    /** @testdox It can have static methods */
    public function testStaticMethods()
    {
        $this->assertEquals("one", MI::asEnglish(1));
    }

The underlying method in the enum is:

public static function asEnglish(int $i) : string
{
    return self::EN[$i - 1];
}
    /** @testdox It can have instance methods */
    public function testInstanceMethods()
    {
        $this->assertEquals("two", MI::RUA->toEnglish());
    }

The underlying method in the enum is:

public function toEnglish() : string
{
    return self::EN[$this->value - 1];
}
    /** @testdox Its name can be returned */
    public function testGetName()
    {
        $this->assertEquals("TORU", MI::TORU->name);
    }

    /** @testdox Its value can be returned */
    public function testGetValue()
    {
        $this->assertEquals(4, MI::WHĀ->value);
    }

I purposedly used the correct accented unicode charactcer in WHÄ€ to check if PHP supported non-ASCII charatcers there. Yes.


    /** @testdox It encodes to JSON OK */
    public function testJsonEncode()
    {
        $this->assertEquals('{"rima":5}', json_encode(["rima" => MI::RIMA]));
    }

    /** @testdox It cannot be type-coerced */
    public function testTypeCoercion()
    {
        $this->expectError(); // NB: not an exception; an error
        $this->expectErrorMessageMatches("/.*MaoriNumbers could not be converted to int.*/");
        $this->assertEquals(sprintf("ono: %d", MI::ONO), "ono: 6");
    }

    /** @testdox It has a from method */
    public function testFrom()
    {
        $this->assertEquals(MI::from(7), MI::WHITU);
    }

    /** @testdox It has a from method that throws an exception */
    public function testFromException()
    {
        $this->expectError(\ValueError ::class);
        $this->expectErrorMessage();
        MI::from(0);
    }

I like how IntelliJ warns me I'm going wrong trying to use 0 there:

    /** @testdox It has a tryFrom method */
    public function testTryFrom()
    {
        $this->assertEquals(MI::tryFrom(8), MI::WARU);
    }

    /** @testdox It has a tryFrom method that returns null */
    public function testTryFromNull()
    {
        $this->assertNull(MI::tryFrom(-1));
    }

    /** @testdox It will type-coerce the argument for from and tryFrom */
    public function testTryFromTypeCoercion()
    {
        $this->assertEquals(MI::from("9"), MI::IWA);
        $this->assertEquals(MI::tryFrom(10.0), MI::TEKAU);
    }

    /** @testdox It can be used in a match expression */
    public function testMatch()
    {
        $this->assertEquals("odd", MI::TAHI->getParity());
        $this->assertEquals("even", MI::RUA->getParity());
    }

The underlying method in the enum is:

public function getParity() : string
{
    return match($this) {
        self::TAHI, self::TORU, self::RIMA, self::WHITU, self::IWA => "odd",
        self::RUA, self::WHĀ, self::ONO, self::WARU, self::TEKAU => "even"
    };
}

Match expressions are new to PHP 8.0, and are an expression version of switch statements. I might need to look at these some more later on too.


    /** @testdox It can have consts, and supply the values for same */
    public function testConsts()
    {
        $this->assertEquals(MI::THREE, MI::TORU);
    }

    /** @testdox It can use traits */
    public function testTraits()
    {
        $this->assertEquals(MI::FOUR, MI::WHĀ);
    }

    /** @testdox It is an object and has a class const */
    public function testClassConst()
    {
        $this->assertEquals(get_class(MI::RIMA), MI::CLASS);
    }

    /** @testdox It supports the __invoke magic method */
    public function testInvoke()
    {
        $this->assertEquals("six", (MI::ONO)());
    }

The underlying method in the enum is:

public function __invoke() : string
{
    return $this->toEnglish();
}

    /** @testdox It has a cases method which returns a listing */
    public function testCases()
    {
        $someCases = array_slice(MI::cases(), 6, 4);
        $this->assertEquals(
            [MI::WHITU, MI::WARU, MI::IWA, MI::TEKAU],
            $someCases
        );

    }

    /** @testdox It can be serialised */
    public function testSerialize()
    {
        $this->assertMatchesRegularExpression(
            sprintf('/^E:\d+:"%s:%s";$/', preg_quote(MI::TAHI::CLASS), MI::TAHI->name ),
            serialize(MI::TAHI)
        );
    }
}

And the whole enum:

namespace adamcameron\php8\tests\Unit\System\Fixtures;

enum MaoriNumbers : int {
    case TAHI = 1;
    case RUA = 2;
    case TORU = 3;
    case WHĀ = 4;
    case RIMA = 5;
    case ONO = 6;
    case WHITU = 7;
    case WARU = 8;
    case IWA = 9;
    case TEKAU = 10;

    use MaoriNumbersConstsTrait;

    private CONST EN = ["one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten"];

    public function toEnglish() : string
    {
        return self::EN[$this->value - 1];
    }

    public static function asEnglish(int $i) : string
    {
        return self::EN[$i - 1];
    }

    public function getParity() : string
    {
        return match($this) {
            self::TAHI, self::TORU, self::RIMA, self::WHITU, self::IWA => "odd",
            self::RUA, self::WHĀ, self::ONO, self::WARU, self::TEKAU => "even"
        };
    }

    public function __invoke() : string
    {
        return $this->toEnglish();
    }
}

This also uses a trait:

namespace adamcameron\php8\tests\Unit\System\Fixtures;

use adamcameron\php8\tests\Unit\System\Fixtures\MaoriNumbers as MI;

trait MaoriNumbersConstsTrait {
    public const ONE = MI::TAHI;
    public const TWO = MI::RUA;
    public const THREE = MI::TORU;
    public const FOUR = MI::WHĀ;
    public const FIVE = MI::RIMA;
    public const SIX = MI::ONO;
    public const SEVEN = MI::WHITU;
    public const EIGHT = MI::WARU;
    public const NINE = MI::IWA;
    public const TEN = MI::TEKAU;
}

I would not normally use a trait (in pretty much any situation), but I created this for the purposes of the testing.

That's it. I just wanted to share my code of my investigations into a language feature I was previously unaware of. I find this a helpful way of learning new stuff: putting it through its paces and formally observing the results.

Righto.

--
Adam

Saturday, 29 October 2022

CFML: addressing confusion around arrays returned from Java methods and using them with CFML code

G'day:

This has come up often enough that it's probably worth having something clear and googleable around for people to find when this crops up for them.

Context

Sometimes it's useful to call a Java method on a CFML object to benefit from functionality that's in Java that's not in CFML. Being able to call split to split a CFML string on a regular expression pattern is a good example:

string = "abcde"
arrayOfChars = string.split("")
writeDump(arrayOfChars)

Lovely.

Problem

So what's the problem? Often there isn't one. However for people who prefer using CFML's object.method() syntax rather than its function(object) syntax, they might get a surprise:

nextChars = arrayOfChars.map((char) => chr(asc(char) + 1))

This results in:

The map method was not found.
Either there are no methods with the specified method name and argument types
or the map method is overloaded with argument types that ColdFusion cannot decipher reliably.
ColdFusion found 0 methods that match the provided arguments.
If this is a Java object and you verified that the method exists, use the javacast function to reduce ambiguity.

Or Lucee (less info, slightly more helpful as to what the issue is):

No matching Method/Function for [Ljava.lang.String;.map(lucee.runtime.type.Lambda) found

What's the problem? Look at the Lucee error. It's pointing out that arrayOfChars is a [Ljava.lang.String (a Java String[]). So that code is trying to call CFML's map method on a Java String[]. Java String[] doesn't have a map method. one has to recall that when one is using object.method() syntax then the type of the object is what dictates what methods can be called. It's not the same with function(object) when function's implementation can take a "close enough" type and cast it to the type it actually needs.

"Workaround"

It's important to note that this would work no worries:

nextChars = arrayMap(arrayOfChars, (char) => chr(asc(char) + 1))

Solution

However sometimes one might want an actual CFML array (so the object passes type-checks, etc), and this is easy enough to achieve:

arrayOfChars = arrayNew(1).append(arrayOfChars, true)

Here we are creating a CFML array, and then using its append method - which will take a Java array and cast it to a CFML array internally - before appending it to the array it's being called on. Job done. Oh the true argument there just means to append the arrays together, rather than appending the first argument into the last element of the first array. Try it yerself to see the difference: pass it false instead.

Summary / proof

string = "abcde"
arrayOfCharsFromSplit = string.split("")

arrayOfCharsAfterAppend = arrayNew(1).append(arrayOfCharsFromSplit, true)

nextChars = arrayOfCharsAfterAppend.map((char) => chr(asc(char) + 1))

writeDump([
    values = [
        arrayOfCharsFromSplit = arrayOfCharsFromSplit,
        arrayOfCharsAfterAppend = arrayOfCharsAfterAppend,
        nextChars = nextChars
    ], types = [
        arrayOfCharsFromSplit = arrayOfCharsFromSplit.getClass().getName(),
        arrayOfCharsAfterAppend = arrayOfCharsAfterAppend.getClass().getName(),
        nextChars = nextChars.getClass().getName()
    ]
])

And the code is in a trycf.com gist today.

Righto.

--
Adam

Wednesday, 14 September 2022

CFML: working MySQL datasource in Application.cfc (this is just a note-to-self/Google)

G'day:

No content in this one, I just want something I can find when I search for how to config a MySQL datasource in Application.cfc in ColdFusion. I always forget/lose the code, and I can never find anything on Google. It'd be marvellous of Adobe would document this stuff, but it's a bit much to expect of them, I guess.

component {

    setsettings()
    loadDatasources()

    private void function setSettings() {
        this.name = "app1"
    }

    private void function loadDataSources() {
        this.datasources["dsn1"] = {
            driver = "mysql",
            class = "com.mysql.jdbc.Driver",
            url = "jdbc:mysql://database.backend:3306/"
                & "#server.system.environment.MARIADB_DATABASE#"
                & "?useUnicode=true&characterEncoding=UTF-8",
            username = server.system.environment.MARIADB_USER,
            password = server.system.environment.MARIADB_PASSWORD
        }
        this.datasource = "dsn1"
    }
}

The bit I always forget is the class bit, so I end up with an error along these lines:

No driver or URL found.

java.sql.SQLException: No driver or URL found.
 at coldfusion.server.j2ee.sql.pool.JDBCPool.createPhysicalConnection(JDBCPool.java:586)
 at coldfusion.server.j2ee.sql.pool.ConnectionRunner$RunnableConnection.run(ConnectionRunner.java:67)
 at java.base/java.lang.Thread.run(Thread.java:834)

This is also on GitHub @ Application.cfc.

Righto.

--
Adam

Wednesday, 31 August 2022

CFML: outputting text from within inline Java in CFML

G'day:

I saw an odd question today. Me mate Ray was messing around with the Java integration in ColdFusion (that allows one to write Java code directly in one's CFML code), and asked how to write out content to the screen from within the Java code. IE: the equiv of emiting some text in one's CFML response… it ends up on the client browser "screen" (this is not my wording).

My initial reaction was "isn't the answer System.out.println("G'day Ray!")?". But it's not. System.out writes to stdout. Not the response buffer. In my Docker container if I do that println, I get it in my Docker log, eg:

OK so it's not a dumb question.

I knew the answer would be "stick it in the PageContext's BodyContent", but I had no idea of how to get a reference to that via Java. Especially basically a Java snippet running inside a ColdFusion request. I googled a lot and did not find anything I understood (my Java is shit).

So I cheated. ColdFusion knows how to get the current PageContext: via getPageContext(). I'll just pass that in to my Java code:

Warning

This code runs fine in the version of CF that is installed from the adobecoldfusion/coldfusion2021:latest Docker image, but does not run on a local install of CF (even the "exact same version"). It will give you a The setPageContext method was not found. error. I'm not sure why, but I managed to grab the ear of one of the Adobe bods, and they are actively looking into it. I'll report back when we get an answer on this.

messager = java {

    import javax.servlet.jsp.PageContext;

    public class Outputer {

        PageContext pageContext;

        public void output(String text) throws java.io.IOException {
            this.pageContext.getOut().print(text);
        }

        public void setPageContext(PageContext pageContext) {
            this.pageContext = pageContext;
        }
    }
}

messager.setPageContext(getPageContext())
writeOutput("Before stuff added from Java<br>")
messager.output("G'day Ray!<br>")
writeOutput("After stuff added from Java<br>")

And this works:

Before stuff added from Java
G'day Ray!
After stuff added from Java

I dunno if it's a good way of doing it, but it's "a" way.

I'd still prefer knowing how to get the PageContext directly from the Java code, if anyone knows…?

Righto.

--
Adam

Wednesday, 25 May 2022

A super-quick Observer Pattern implementation in CFML, and I skip TDD. Or do I?

G'day:

There's a possible "Betteridge's law of headlines" situation there. I'm not actually sure yet.

A few days back I was chatting to someone about application-scope-usage, and how to trigger an event when any changes to the application scope took place. I mentioned that the application scope should never be accessed directly in one's application code, and it should be wrapped up in an adapter. And it's easy for one's adapter to trigger events if needs must. An implementation of the Observer Pattern would do the trick here.

Then it occurred to me that I have never actually implemented the Observer Pattern, and I thought I might give it a go.

I've read that Wiki article a few times, and know what it needs to do, but hadn't thought it through at all before. So I opened a file and settled in to work out what I'd need to do. Then I paused and went "Adam: tests. Don't let yerself down here". Grumble. But I reasoned that I didn't even know if I was gonna go anywhere with this code, so I decided to call it a spike (random code which will be thrown away, and no tests).

I decided to implement it as an ObserverService: an instance of it would be injected into other objects that needed to either attach a handler to an event, or trigger an event. Internally the service would have to maintain some data structure of events and handlers, and probably only needed a coupla methods. Following jQuery (my JS is very out of date, I admit this), I decided on on for the method to attach a handler to an event; and trigger to fire the event (and run the handlers). As I type this, I realise I'm already thinking of the bases for test cases. I shoulda just done the tests up front. Or at least made the test cases, eg:

describe("test for the on method",  () => {
    it("adds an event handler to an event", () => {
    })
})

describe("test for the trigger method",  () => {
    it("runs all the event handlers for the event", () => {
    })
})

Oh well.

Right so I spiked away, and it all came together rather more easily than I expected. Here's the first pass (untested):

component {

    variables.eventRegistry = {}

    public void function on(required string event, required function handler) {
        registerEvent(event)
        variables.eventRegistry[event].append(handler)
    }

    public boolean function trigger(required string event) {
        registerEvent(event)
        return variables.eventRegistry[event].some((handler) => handler())
    }

    private void function registerEvent(required string event) {
        if (!variables.eventRegistry.keyExists(event)) {
            variables.eventRegistry[event] = []
        }
    }
}

That seems to have everything I need to start with:

  • I can attach a handler to an event.
  • I can trigger the event, causing the handlers to run in sequence.
  • If any handler returns false, stop calling handlers.
  • A bug that never would have cropped up had I done TDD properly.

Right so the way to check if this thing works is still via tests, so I knocked some tests together now. I'll just list the cases, as the implementations don't matter so much. I wrote all of these before running any of them (on purpose because I was being a dick, not cos it's good practice).

import cfml.forBlog.observerService.ObserverService

component extends=Testbox.system.BaseSpec {
    function run() {
        describe("Tests for ObserverService", () => {
            describe("Tests for on method", () => {
                it("registers an event handler", () => {
                })

                it("registers multiple event handlers", () => {
                })

                it("registers multiple handlers for multiple events", () => {
                })
            })

            describe("tests for trigger method", () => {
                it("triggers a registered event", () => {
                })

                it("does nothing when triggering an unregistered event", () => {
                })

                it("triggers event handlers until one returns false", () => {
                })
            })
        })
    }
}

And I triumphantly ran them, and then…

… I did not feel very triumphant any more. That was the result of every single test. So that's the bug. Did you spot it, or can you see it now?

return variables.eventRegistry[event].some((handler) => handler())

The handlers I was attaching to the event didn't necessarily return true if they worked. Just "not erroring" is a good enough sign an event handler ran OK. EG:

observerService.on("testEvent", () => {
    eventResults.append("test event")
})

So here handler() returns an array. No good as a boolean.

This was easily fixed:

return variables.eventRegistry[event].some((handler) => handler() == false)

Had I done TDD I'd've first done the minimum implementation without the "returning false from a handler terminates the handler loop" feature, then I would have added a test for the feature, and then my attention would have been focused on what I was actually doing, and I'd not have made such a stupid mistake.

Ah well.

After that first iteration, I realised I needed to also be able to pass data to the handler, so I did that too. Via TDD this time. This was the end result:

component {

    variables.eventRegistry = {}

    public void function on(required string event, required function handler, any data) {
        registerEvent(event)
        variables.eventRegistry[event].append({
            handler = handler,
            data = arguments?.data
        })
    }

    public boolean function trigger(required string event) {
        registerEvent(event)
        return variables.eventRegistry[event].some((eventData) => eventData.handler({name=event, data=eventData?.data}) == false)
    }

    private void function registerEvent(required string event) {
        if (!variables.eventRegistry.keyExists(event)) {
            variables.eventRegistry[event] = []
        }
    }
}

I'm pretty pleased with this simple little solution. It does what it needs to do, and is only ~700 bytes of code.

I'll get to why I'm creating this thing in a coupla days, showing how I'll use dependency injection to inject an instance of it into both publisher and subscribers of events, and how I solve that "firing an event every time the application scope is changed" challenge I mentioned in the beginning of this article.

Righto.

--
Adam

Sunday, 15 May 2022

CFML: fixing a coupla bugs in my recent work on TinyTestFramework

G'day:

Last week I did some more work on my TinyTestFramework:

On Saturday, I found a bug in each of those. Same bug, basically, surfacing in two different ways. Here's an example:

describe("Demonstrating afterEach bug", () => {
    afterEach(() => {
        writeOutput("<br><br>This should be displayed<br><br>")
    })

    describe("Control", () => {
        it("is a passing test, to demonstrate expected behaviour", () => {
            expect(true).toBeTrue()
        })        
    })

    describe("Demonstrating bug", () => {
        it("should run even if the test fails", () => {
            expect(true).toBeFalse()
        })
    })
})

Output:

Demonstrating afterEach bug
Control
It is a passing test, to demonstrate expected behaviour:

This should be displayed

OK
Demonstrating bug
It should run even if the test fails: Failed
Results: [Pass: 1] [Fail: 1] [Error: 0] [Total: 2]

Note how the second This should be displayed is not being displayed. Why's this? It's because, internally, a failing test throws an exception:

toBeTrue = () => tinyTest.matchers.toBe(true, actual),

// ...

toBe = (expected, actual) => {
    if (actual.equals(expected)) {
        return true
    }
    throw(type="TinyTest.TestFailedException")
},

And in the implementation of it, an exception is caught before the code handling afterEach has a chance to run:

it = (string label, function implementation) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("It #label#: ")

            tinyTest.contexts
                .filter((context) => context.keyExists("beforeEachHandler"))
                .each((context) => context.beforeEachHandler())

            decoratedImplementation = tinyTest.contexts
                .filter((context) => context.keyExists("aroundEachHandler"))
                .reduce((reversed, context) => reversed.prepend(context), [])
                .reduce((decorated, context) => () => context.aroundEachHandler(decorated), implementation)
            decoratedImplementation()

            tinyTest.contexts
                .filter((context) => context.keyExists("afterEachHandler"))
                .reduce((reversedContexts, context) => reversedContexts.prepend(context), [])
                .each((context) => context.afterEachHandler())

            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},

To explain:

  • implementation is the callback from the it in the test suite. The actual test.
  • All that filter / reduce stuff is just handling aroundEach: don't worry about that.
  • After the decoration we run the test.
  • If it fails, it is caught down here.
  • Meaning the handling of the afterEach callback is never run.

This seems fairly easy to sort out:

try {
    decoratedImplementation()
} finally {
    tinyTest.contexts
        .filter((context) => context.keyExists("afterEachHandler"))
        .reduce((reversedContexts, context) => reversedContexts.prepend(context), [])
        .each((context) => context.afterEachHandler())
}

Now even if the test fails, the afterEachHandler handler will still be run. And as I'm not catching the exception, it'll still do what it was supposed to. Rerunning the tests demonstrates this bug is fixed:

Demonstrating afterEach bug
Control
It is a passing test, to demonstrate excpected behaviour:

This should be displayed

OK
Demonstrating bug
It should run even if the test fails:

This should be displayed

Failed
Results: [Pass: 1] [Fail: 1] [Error: 0] [Total: 2]

I also ran all the rest of the tests as well, and they all still pass, so I'm pretty confident my fix has had no repercussions.


I've got the same problem with aroundEach: the bit of the handler after the call to run the test was not being run, for the same reason we had with afterEach: a failing or erroring test throws an exception, and the exception is caught before the rest of the aroundEach handler can be run. This seems slightly trickier to handle, as the code to call the test is within the callback the tester provides:

aroundEach((test) => {
    // top bit before calling the test. No problem with this

    test()

    // bottom bit. This is not getting run after a failed / erroring test
})

I can't expect the testing dev to stick handling of the test failure in there. I need to do this within the framework.

How to do this flummoxed me a bit, but I wrote some tests in the mean time to give my brain some time to think about things:

describe("Demonstrating aroundEach bug", () => {
    aroundEach((test) => {
        writeOutput("<br><br>Before the call to the test: this should be displayed<br>")
        test()
        writeOutput("<br>After the call to the test:This should be displayed<br><br>")
    })

    describe("Control", () => {
        it("is a passing test, to demonstrate expected behaviour", () => {
            expect(true).toBeTrue()
        })        
    })

    describe("Demonstrating bug", () => {
        it("should display the 'bottom' message even if the test fails", () => {
            expect(true).toBeFalse()
        })
    })
})

Results:

Demonstrating aroundEach bug
Control
It is a passing test, to demonstrate expected behaviour:

Before the call to the test: this should be displayed

After the call to the test: This should be displayed

OK
Demonstrating bug
It should display the 'bottom' message even if the test fails:

Before the call to the test: this should be displayed
Failed
Results: [Pass: 1] [Fail: 1] [Error: 0] [Total: 2]

See how the second test isn't outputting After the call to the test: This should be displayed.

it = (string label, function implementation) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("It #label#: ")

            tinyTest.contexts
                .filter((context) => context.keyExists("beforeEachHandler"))
                .each((context) => context.beforeEachHandler())

            decoratedImplementation = tinyTest.contexts
                .filter((context) => context.keyExists("aroundEachHandler"))
                .reduce((reversed, context) => reversed.prepend(context), [])
                .reduce((decorated, context) => () => context.aroundEachHandler(decorated), implementation)

        try {    
            decoratedImplementation()
        } finally {
            tinyTest.contexts
                .filter((context) => context.keyExists("afterEachHandler"))
                .reduce((reversedContexts, context) => reversedContexts.prepend(context), [])
                .each((context) => context.afterEachHandler())
        }

            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},

The culmination of that deocration code is how any aroundEach handler is called around the test implementation. Somehow I need to do the equivalent of that try / finally here. But it's not so straight forward as I basically need to prevent that call to implementation from erroring until after we bubble out of all the aroundEach handlers. Bear in mind there can be any number of aroundEach handlers to run: one for each level of describe in the tests:

describe("Test of a CFC", () => {

    aroundEach((test) => {
        // something before
        test()
        // something afterwards
    })

    describe("Test of a method", () => {

        aroundEach((test) => {
            // something before
            test()
            // something afterwards
        })

        describe("Test of a specific part of the method's behaviour", () => {

            aroundEach((test) => {
                // something before
                test()
                // something afterwards
            })
            
            it("will have all three of those `aroundEach` handlers run around it",  () => {
                // test stuff
            })
        })
    })
})

OK so I need to put a try / catch around the call to implementation so i can stop it erroring-out too soon. That's easy enough:

decoratedImplementation = tinyTest.contexts
    .filter((context) => context.keyExists("aroundEachHandler"))
    .reduce((reversed, context) => reversed.prepend(context), [])
    .reduce((decorated, context) => () => context.aroundEachHandler(decorated), () => {
        try {
            implementation()
        } catch (any e) {
            // ???
        }                            
    })
    
try {    
    decoratedImplementation()
    // ???
} finally {
    tinyTest.contexts
        .filter((context) => context.keyExists("afterEachHandler"))
        .reduce((reversedContexts, context) => reversedContexts.prepend(context), [])
        .each((context) => context.afterEachHandler())
}

But I still need to know about that exception after we finish calling the aroundEach handlers and the test implementation.

I'm not sure I like this implementation, but this is what I have done:

decoratedImplementation = tinyTest.contexts
    .filter((context) => context.keyExists("aroundEachHandler"))
    .reduce((reversed, context) => reversed.prepend(context), [])
    .reduce((decorated, context) => () => context.aroundEachHandler(decorated), () => {
        try {
            implementation()
            tinyTest.testResult = true
        } catch (any e) {
            tinyTest.testResult = e
        }                            
    })
    
try {    
    decoratedImplementation()
    if (!tinyTest.testResult.equals(true)) {
        throw(object=tinyTest.testResult)
    }
} finally {

I set a variable in the calling code either flagging the test worked, or if not: how it failed (or errored). The if it failed, I throw the exception I originally caught.

This works, and both those tests I wrote above, and all the rest of the test suite still passes too. Bug fixed.

I'm still thinking about this though. I feel I have nailed the red/green part of this process, but I still possibly have some refactoring to do. But obvs now I'm safe to do so because everything is tested. Well: except any other bugs I haven't noticed yet :-)

Righto.

--
Adam

Thursday, 12 May 2022

CFML: Adding beforeEach handlers to my TinyTestFramework. Another exercise in TDD

G'day:

I have to admit I'm not sure where I'm going with this one yet. I dunno how to implement what I'm needing to do, but I'm gonna start with a test and see where I go from there.

Context: I've been messing around with this TinyTestFramework thing for a bit… it's intended to be a test framework one can run in trycf.com, so I need to squeeze it all into one include file, and at the same time make it not seem too rubbish in the coding dept. The current state of affairs is here: tinyTestFramework.cfm, and its tests: testTinyTestFramework.cfm. Runnable here: on trycf.com

The next thing that has piqued my interest for this is to add beforeEach and afterEach handlers in there too. This will be more of a challenge than the recent "add another matcher" carry on I've done.

First test:

describe("Tests of beforeEach", () => {
    result = ""
    beforeEach(() => {
        result = "set in beforeEach handler"
    })
    
    it("was called before the first test in the set", () => {
        expect(result).toBe("set in beforeEach handler")
    })
})

Right and the first implementation doesn't need to be clever. Just make it pass:

tinyTest = {
    // ...
    beforeEach = (callback) => {
        callback()
    }
}

// ...
beforeEach = tinyTest.beforeEach

This passes. Cool.

That's fine but it's a bit daft. My next test needs to check that beforeEach is called before subsequent tests too. To test this, simply setting a string and checking it's set won't be any use: it'll still be set in the second test too. Well: either set or reset… no way to tell. So I'll make things more intelligent (just a bit):

describe("Tests of beforeEach", () => {
    result = []
    beforeEach(() => {
        result.append("beforeEach")
    })
    
    it("was called before the first test in the set", () => {
        result.append("first test")
        
        expect(result).toBe([
            "beforeEach",
            "first test"
        ])
    })
    
    it("was called before the second test in the set", () => {
        result.append("second test")
        
        expect(result).toBe([
            "beforeEach",
            "first test",
            "beforeEach",
            "second test"
        ])
    })
})

Now each time beforeEach is called it will cumulatively affect the result, so we can test that it's being called for each test. Which of course it is not, currently, so the second test fails.

Note: it's important to consider that in the real world having beforeEach cumulatively change data, and having the sequence the tests are being run be significant - eg: we need the first test to be run before the second test for either test to pass - is really bad form. beforeEach should be idempotent. But given it's what we're actually testing here, this is a reasonable way of testing its behaviour, I think.

Right so currently we are running the beforeEach callback straight away:

beforeEach = (callback) => {
    callback()
}

It needs to be cleverer than that, and only be called when the test is run, which occurs inside it:

it = (string label, function implementation) => {
    tinyTest.inDiv(() => 
        try {
            writeOutput("It #label#: ")
            implementation()
            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},

The beforeEach call just has to stick the callback somewhere for later. Hrm. OK:

beforeEachHandler = false,
beforeEach = (callback) => {
    tinyTest.beforeEachHandler = callback
},
it = (string label, function implementation) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("It #label#: ")

            tinyTest.beforeEachHandler()
            
            implementation()

            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},

That works. Although it's dangerously fragile, as that's gonna collapse in a heap if I don't have a beforeEach handler set. I've put this test before those other ones:

describe("Tests without beforeEach", () => {
    it("was called before the first test in the set", () => {
        expect(true).toBe(true)
    })
})

And I get:

Tests of TinyTestFramework
Tests without beforeEach
It was called before the first test in the set: Error: [The function [beforeEachHandler] does not exist in the Struct, only the following functions are available: [append, clear, copy, count, delete, duplicate, each, every, filter, find, findKey, findValue, insert, isEmpty, keyArray, keyExists, keyList, keyTranslate, len, map, reduce, some, sort, toJson, update, valueArray].][]
Tests of beforeEach
It was called before the first test in the set: OK
It was called before the second test in the set: OK
Results: [Pass: 2] [Fail: 0] [Error: 1] [Total: 3]

I need a guard statement around the call to the beforeEach handler:

if (isCustomFunction(tinyTest.beforeEachHandler)) {
    tinyTest.beforeEachHandler()
}

That fixed it.

Next I need to check that the beforeEach handler cascades into nested describe blocks. I've a strong feeling this will "just work":

describe("Tests of beforeEach", () => {
    describe("Testing first level implementation", () => {
        // (tests that were already in place now in here)
    })
    describe("Testing cascade from ancestor", () => {
        result = []
        beforeEach(() => {
            result.append("beforeEach in ancestor")
        })
        describe("Child of parent", () => {
            it("was called even though it is in an ancestor describe block", () => {
                result.append("test in descendant")
                
                expect(result).toBe([
                    "beforeEach in ancestor",
                    "test in descendant"
                ])
            })
        })
    })
})

Note that I have shunted the first lot of tests into their own block now. Also: yeah, this already passes, but I think it's a case of coincidence rather than good design. I'll add another test to demonstrate this:

describe("Tests without beforeEach (bottom)", () => {
    result = []
    it("was called after all other tests", () => {
        result.append("test after any beforeEach implementation")
        
        expect(result).toBe([
            "test after any beforeEach implementation"
        ])
    })
})

This code is right at the bottom of the test suite. If I put a writeDump(result) in there, we'll see why:

implentation (sic) error

After I pressed send on this, I noticed the typo in the test and in the dump above. I fixed the test, but can't be arsed fixing the screen cap. Oops.

You might not have noticed, but I had not VARed that result variable: it's being used by all the tests. This was by design so I could test for leakage, and here we have some: tinyTest.beforeEachHandler has been set in the previous describe block, and it's still set in the following one. We can't be having that: we need to contextualise the handlers to only be in-context within their original describe blocks, and its descendants.

I think all I need to do is to get rid of the handler at the end of the describe implementation:

describe = (string label, function testGroup) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("#label#<br>")
            testGroup()
            tinyTest.beforeEachHandler = false
        } catch (any e) {
            writeOutput("Error: #e.message#<br>")
        }
    })
},

The really seemed easier than I expected it to be. I have a feeling this next step is gonna be trickier though: I need to be able to support multiple sequential handlers, like this:

describe("Multiple sequential handlers", () => {
    beforeEach(() => {
    	result = []
        result.append("beforeEach in outer")
    })
    describe("first descendant of ancestor", () => {
        beforeEach(() => {
            result.append("beforeEach in middle")
        })
        describe("inner descendant of ancestor", () => {
            beforeEach(() => {
                result.append("beforeEach in inner")
            })
            it("calls each beforeEach handler in the hierarchy, from outermost to innermost", () => {
                result.append("test in innermost descendant")
                
                expect(result).toBe([
                    "beforeEach in outer",
                    "beforeEach in middle",
                    "beforeEach in inner",
                    "test in innermost descendant"
                ])
            })
        })
    })
})

Here we have three nested beforeEach handlers. This fails because we're only storing one, which we can see if we do a dump in the test:

I guess we need to chuck these things into an array instead:

describe = (string label, function testGroup) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("#label#<br>")
            testGroup()
           
        } catch (any e) {
            writeOutput("Error: #e.message#<br>")
        } finally {
            tinyTest.beforeEachHandlers = []
        }
    })
},
beforeEachHandlers = [],
beforeEach = (callback) => {
    tinyTest.beforeEachHandlers.append(callback)
},
it = (string label, function implementation) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("It #label#: ")

            tinyTest.beforeEachHandlers.each((handler) => {
                handler()
            })

            implementation()

            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},

This makes the tests pass, but I know this bit is wrong:

tinyTest.beforeEachHandlers = []

If I have a second test anywhere in that hierarchy, the handlers will have been blown away, and won't run:

describe("Multiple sequential handlers", () => {
    beforeEach(() => {
        result = []
        result.append("beforeEach in outer")
    })
    describe("first descendant of ancestor", () => {
        beforeEach(() => {
            result.append("beforeEach in middle")
        })

        describe("inner descendant of ancestor", () => {
            beforeEach(() => {
                result.append("beforeEach in inner")
            })
            it("calls each beforeEach handler in the hierarchy, from outermost to innermost", () => {
                result.append("test in innermost descendant")

                expect(result).toBe([
                    "beforeEach in outer",
                    "beforeEach in middle",
                    "beforeEach in inner",
                    "test in innermost descendant"
                ])
            })
        })

        it("is a test in the middle of the hierarchy, after the inner describe", () => {
            result.append("test after the inner describe")

            expect(result).toBe([
                "beforeEach in outer",
                "beforeEach in middle",
                "after the inner describe"
            ])
        
        })
        
    })
})

This fails, and a dump shows why:

So I've got no handlers at all (which is correct given my current implementation), but it should still have the "beforeEach in outer" and "beforeEach in middle" handlers for this test. I've deleted too much. Initially I was puzzled why I still had all that stuff in the result still, but then it occurs to me that was the stuff created for the previous test, just with my last "after the inner describe" appended. So that's predictable/"correct" for there being no beforeEach handlers running at all.

I had to think about this a bit. Initially I thought I'd need to concoct some sort of hierarchical data structure to contain the "array" of handlers, but after some thought I think an array is right, it's just that I only need to pop off the last handler, and only if it's the one set in that describe block. Not sure how I'm gonna work that out, but give me a bit…

OK, I think I've got it:

contexts = [],
describe = (string label, function testGroup) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("#label#<br>")
            tinyTest.contexts.push({})
            testGroup()
           
        } catch (any e) {
            writeOutput("Error: #e.message#<br>")
        } finally {
            tinyTest.contexts.pop()
        }
    })
},
beforeEach = (callback) => {
    tinyTest.contexts.last().beforeEachHandler = callback
},
it = (string label, function implementation) => {
    tinyTest.inDiv(() => {
        try {
            writeOutput("It #label#: ")

            tinyTest.contexts.each((context) => {
                context.keyExists("beforeEachHandler") ? context.beforeEachHandler() : false
            })

            implementation()

            tinyTest.handlePass()
        } catch (TinyTest e) {
            tinyTest.handleFail()
        } catch (any e) {
            tinyTest.handleError(e)
        }
    })
},
  • I maintain an array of contexts.
  • At the beginning of each describe handler I create a context for it - which is just a struct, and push it onto the contexts array.
  • A beforeEach call sticks its handler into the last context struct, which will be the one for the describe that the beforeEach call was made in.
  • When it runs, it iterates over contexts.
  • And if there's a beforeEach handler in a context, then its run.
  • The last thing describe does is to remove its context from the context array.

This means that as each describe block in a hierarchy is run, it "knows" about all the beforeEach handlers created in its ancestors, and during its own run, it adds its own context to that stack. All tests immediately within it, and within any descendant describe blocks will have all the beforeEach handlers down it and including itself. Once it's done, it tidies up after itself, so any subsequently adjacent describe blocks start with on the the their ancestor contexts.

Hopefully one of the code itself, the bulleted list or the narrative paragraph explained what I mean.

As well as the tests I had before this implementation, I added tests for another few scenarios too. Basically any combination / ordering / nesting of describe / it calls I could think of, testing the correct hierarchical sequence of beforeEach handlers was called in the correct order, for the correct test, without interfering with any other test.

describe("Multiple sequential handlers", () => {
    beforeEach(() => {
        result = []
        result.append("beforeEach in outer")
    })
    
    it("is at the top of the hierarchy before any describe", () => {
        result.append("at the top of the hierarchy before any describe")
        
        expect(result).toBe([
            "beforeEach in outer",
            "at the top of the hierarchy before any describe"
        ])
    })
    
    describe("first descendant of ancestor", () => {
        beforeEach(() => {
            result.append("beforeEach in middle")
        })

        it("is a test in the middle of the hierarchy, before the inner describe", () => {
            result.append("test before the inner describe")

            expect(result).toBe([
                "beforeEach in outer",
                "beforeEach in middle",
                "test before the inner describe"
            ])
        })

        describe("inner descendant of ancestor", () => {
            it("is a test in the bottom of the hierarchy, before the inner beforeEach", () => {
                result.append("in the bottom of the hierarchy, before the inner beforeEach")

                expect(result).toBe([
                    "beforeEach in outer",
                    "beforeEach in middle",
                    "in the bottom of the hierarchy, before the inner beforeEach"
                ])
            })
            beforeEach(() => {
                result.append("beforeEach in inner")
            })
            it("calls each beforeEach handler in the hierarchy, from outermost to innermost", () => {
                result.append("test in innermost descendant")

                expect(result).toBe([
                    "beforeEach in outer",
                    "beforeEach in middle",
                    "beforeEach in inner",
                    "test in innermost descendant"
                ])
            })
            it("is another innermost test", () => {
                result.append("is another innermost test")

                expect(result).toBe([
                    "beforeEach in outer",
                    "beforeEach in middle",
                    "beforeEach in inner",
                    "is another innermost test"
                ])
            })
        })

        it("is a test in the middle of the hierarchy, after the inner describe", () => {
            result.append("test after the inner describe")

            expect(result).toBe([
                "beforeEach in outer",
                "beforeEach in middle",
                "test after the inner describe"
            ])
        })
    })
    
    describe("A second describe in the middle tier of the hierarchy", () => {
        beforeEach(() => {
            result.append("beforeEach second middle describe")
        })

        it("is a test in the second describe in the middle tier of the hierarchy", () => {
            result.append("in the second describe in the middle tier of the hierarchy")

            expect(result).toBe([
                "beforeEach in outer",
                "beforeEach second middle describe",
                "in the second describe in the middle tier of the hierarchy"
            ])
        })
    })
    
    it("is at the top of the hierarchy after any describe", () => {
        result.append("at the top of the hierarchy after any describe")
        
        expect(result).toBe([
            "beforeEach in outer",
            "at the top of the hierarchy after any describe"
        ])
    })
})

All are green, and all the other tests are still green as well. Yay for the testing safety-net that TDD provides for one. I think I have implemented beforeEach now. Implementing afterEach is next, but this should be easy, and just really the same as I have done here, with similar tests.

However I will do that separate to this, and I am gonna press "send" on this, have a beer first.

Code:

Oh: the code:test ratio is now 179:713, or around 1:4.

Righto.

--
Adam

Saturday, 7 May 2022

Running CFML code on trycf.com via a remote HTTP request

G'day:

This ended up being more of a rabbit hole than I expected it to be. But in the process I've learned a bit more about curl, PHP, Python, JS (client). And actually CFML too I guess.

I can't even remember why I needed to do this, but it was something to do with testing that TinyTestFramework I've been blathering about recently.

Anyhow, I decided I needed to run some code locally on my PC which would send some code off to trycf.com, run it, and send me back the response. I figured it'd be doable if I worked out what Abram was doing when I click the "Run Code" button on the trycf.com UI. As it turns out it's just an HTTP post, and I could could re-run the curl captured from my browser easily enough:

Yeah I don't care

Thanks, but before you take time to mention it: I know the code blows out to the right. It doesn't matter, no-one's expecting you to read it really, and the blow-out is just just cosmetic shite.

curl 'https://lucee5-sbx.trycf.com/lucee5/getremote.cfm' \
  -H 'authority: lucee5-sbx.trycf.com' \
  -H 'accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9' \
  -H 'accept-language: en-GB,en-US;q=0.9,en;q=0.8' \
  -H 'cache-control: max-age=0' \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundaryaYm5TBPgCaT5n3TK' \
  -H 'cookie: _ga=GA1.2.771352503.1647902596; _gid=GA1.2.1730098774.1651605323; _gat_gtag_UA_35934323_2=1' \
  -H 'dnt: 1' \
  -H 'origin: https://trycf.com' \
  -H 'referer: https://trycf.com/' \
  -H 'sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="100", "Google Chrome";v="100"' \
  -H 'sec-ch-ua-mobile: ?0' \
  -H 'sec-ch-ua-platform: "Windows"' \
  -H 'sec-fetch-dest: iframe' \
  -H 'sec-fetch-mode: navigate' \
  -H 'sec-fetch-site: same-site' \
  -H 'sec-fetch-user: ?1' \
  -H 'upgrade-insecure-requests: 1' \
  -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36' \
  --data-raw $'------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="zoom"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n\r\n</cfscript>\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="postcode"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="key"\r\n\r\nmain1651930152982-9b67997d-643d-3ddf-0b9f-6154282cfcf4\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------WebKitFormBoundaryaYm5TBPgCaT5n3TK--\r\n' \
  --compressed

I can run that in bash and it works fine. BTW, the actual code I'm running is buried in the middle there, There's quite a chunk of overhead to execute that code, and I reckoned the browser was probably being a bit belt-n-braces about all the headers it was sending, and I'd not need most of them. I whittled it down to this:

curl 'https://lucee5-sbx.trycf.com/lucee5/getremote.cfm' \
  -H 'content-type: multipart/form-data; boundary=----__trycf__' \
  --data-raw $'------__trycf__\r\nContent-Disposition: form-data; name="setupcode"\r\n\r\n\r\n------__trycf__\r\nContent-Disposition: form-data; name="code"\r\n\r\n<cfscript>\r\nwriteOutput("hi")\r\n</cfscript>\r\n------__trycf__\r\nContent-Disposition: form-data; name="asserts"\r\n\r\n\r\n------__trycf__--\r\n' \
  --compressed

There's a handy site that converts curls to language-specific implementations, and CFML is one of the options: https://curlconverter.com/#cfml. This was handy in theory, but the HTTP service call it created didn't work. Not its fault: it should have, but it seems CFHTTP can't handle that boundary syntax in the curl. Note that PHP's version also struggled, but the JS (fetch), Java and Python versions all worked fine.

This threw me for a while cos I'm not really that au fait with building HTTP requests by hand, but eventually I cracked it. Don't hand-crank the multipart boundary stuff: let CFML do it for you. So I came up with this proof of concept:

<cfset code = fileRead(expandPath("./code.cfm"))>

<cfhttp method="post" url="https://acf14-sbx.trycf.com/getremote.cfm" result="httpResponse">
    <cfhttpparam type="formField" name="code" value=#code#>
    <cfhttpparam type="formField" name="asserts" value="">
</cfhttp>

<cfif httpResponse.statusCode EQ "200 OK">
    <cfoutput>#httpResponse.fileContent#</cfoutput>
<cfelse>
    <cfdump var="#httpResponse#">
</cfif>

That's pretty simple. And my test code for this is just:

<cfscript>
    name = "Scott Steinbeck"
    writeOutput("G'day #name#")
</cfscript>

Results:

G'day Scott Steinbeck

(It was Scott that asked me to write this up).

That's where I've got to so far. I also want to see how I can include some set-up code. Hang on a sec whilst I watch some more HTTP traffic.

[…]

Oh OK, that was easy:

<cfset framework = fileRead(expandPath("./tinyTestFramework.cfm"))>
<cfset tests = fileRead(expandPath("./tests.cfm"))>

<cfhttp method="post" url="https://acf14-sbx.trycf.com/getremote.cfm" result="httpResponse">
    <cfhttpparam type="formField" name="setupcode" value="#framework#">
    <cfhttpparam type="formField" name="code" value="#tests#">
    <cfhttpparam type="formField" name="asserts" value="">
</cfhttp>

<cfif httpResponse.statusCode EQ "200 OK">
    <cfoutput>#httpResponse.fileContent#</cfoutput>
<cfelse>
    <cfdump var="#httpResponse#">
</cfif>

(code on github)

That example just runs the test for my test framework up on trycf instead of here on my own server. Because I can. At least now I remember why I wanted to do this in the first place, but that will be in a later article.

All these test so far only run on ColdFusion 2021, because that was what I was wanting to do. The other hosts are easy enough to glean just by watching the request when clicking "run code". The Lucee (latest) one is https://lucee5-sbx.trycf.com/lucee5/getremote.cfm

Anyway, not a terribly exciting one this (not like how terribly exciting the shit I write here usually is, eh? EH??), but problem solved and hopefully this will help Scott.

Righto.

--
Adam

Wednesday, 4 May 2022

CFML: updates to my TinyTestFramework

G'day:

Just to pass the time / avoid other things I really ought to be doing instead, over the last few evenings I'm been messing around with my TinyTestFramework. I first created this as an exercise in doing some "real world" TDD for a blog article: "TDD: writing a micro testing framework, using the framework to test itself as I build it". The other intent of this work is so I can run actual tests in my code on trycf.com. This is useful when I'm both asking and answering CFML questions I encounter on the CFML Slack and other places.

The first iteration of the framework was pretty minimal. It was just this:

void function describe(required string label, required function testGroup) {
    try {
        writeOutput("#label#<br>")
        testGroup()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}
void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#: ")
        implementation()
        writeOutput("OK<br>")
    } catch (TestFailedException e) {
        writeOutput("Failed<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}
function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TestFailedException")
    }}
}

That's it.

But it let me write tests in a Jasmine/TestBox sort of way, right there in trycf.com:

describe("describe", () => {
    it("it is a test", () => {
        expect(true).toBe(true)
    })
}) 

And this would output:

describe
it is a test: OK

That's cool. That was a good MVP. And I actually use it on trycf.com.

However I quickly felt that only having the one toBe matcher was limiting, and made my tests less clear than they could be. Especially when I wanted to expect null or an exception. So… I messed around some more.

I'm not going to take you through the full TDD exercise of writing all this, but I assure you I TDDed almost all of it (I forgot with a coupla small tweaks, I have to admit. I'm not perfect).

But here's the code (also as a gist), for those that are interested:

<style>
    .tinyTest {background-color: black; color:white; font-family:monospace}
    .tinyTest div {margin-left: 1em}
    .tinyTest .pass {color:green;}
    .tinyTest .fail {color:red;}
    .tinyTest .error {background-color:red; color:black}
</style>
<cfscript>
function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TinyTest.TestFailedException")
    }}
}

tinyTest = {
    describe = (string label, function testGroup) => {
        tinyTest.inDiv(() => {
            try {
                writeOutput("#label#<br>")
                testGroup()
            } catch (any e) {
                writeOutput("Error: #e.message#<br>")
            }
        })
    },
    it = (string label, function implementation) => {
        tinyTest.inDiv(() => {
            try {
                writeOutput("It #label#: ")
                implementation()
                tinyTest.showPass("OK<br>")
            } catch (TinyTest e) {
                tinyTest.showFail("Failed<br>")
            } catch (any e) {
                tinyTest.showError("Error: #e.message#<br>")
            }
        })
    },
    expect = (any actual) => {
        var proxy.actual = arguments?.actual
    
        return {
            toBe = (expected) => tinyTest.matchers.toBe(expected, actual),
            toBeTrue = () => tinyTest.matchers.toBe(true, actual),
            toBeFalse = () => tinyTest.matchers.toBe(false, actual),
            toBeNull = () => tinyTest.matchers.toBeNull(proxy?.actual),
            toThrow = () => tinyTest.matchers.toThrow(actual),
            toInclude = (needle) => tinyTest.matchers.toInclude(needle, actual),
            notToBe = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(expected, actual)),
            notToBeTrue = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(true, actual)),
            notToBeFalse = (expected) => tinyTest.matchers.not((expected) => tinyTest.matchers.toBe(false, actual)),
            notToBeNull = () => tinyTest.matchers.not(() => tinyTest.matchers.toBeNull(proxy?.actual)),
            notToThrow = () => tinyTest.matchers.not(() => tinyTest.matchers.toThrow(actual)),
            notToInclude = (needle) => tinyTest.matchers.not((needle) => tinyTest.matchers.toInclude(needle, actual))
            
        }
    },
    fail = () => {
        throw(type="TinyTest.FailureException")
    },
    matchers = {},
    inDiv = (callback)  => {
        writeOutput("<div>")
        callBack()
        writeOutput("</div>")
    },
    showPass = (message) => {
        writeOutput('<span class="pass">#message#</span>')
    },
    showFail = (message) => {
        writeOutput('<span class="fail"><em>#message#</em></span>')
    },
    showError = (message) => {
        writeOutput('<span class="error"><strong>#message#</strong></span>')
    }
    
}
tinyTest.matchers.toBe = (expected, actual) => {
    if (actual.equals(expected)) {
        return true
    }
    throw(type="TinyTest.TestFailedException")
}
tinyTest.matchers.toBeNull = (actual) => {
    if (isNull(actual)) {
        return true
    }
    throw(type="TinyTest.TestFailedException")
}
tinyTest.matchers.toThrow = (callback) => {
    try {
        callback()
        throw(type="TinyTest.TestFailedException")
    } catch (TinyTest.TestFailedException e) {
        rethrow
    } catch (any e) {
        return true
    }
}
tinyTest.matchers.toInclude = (needle, haystack) => tinyTest.matchers.toBe(true, haystack.findNoCase(needle) > 0)

tinyTest.matchers.not = (callback) => {
    try {
        callback()
        throw(type="TinyTest.NotTestFailedException")
    } catch (TinyTest.NotTestFailedException e) {
        throw(type="TinyTest.TestFailedException")
    } catch (TinyTest.TestFailedException e) {
        return true
    } catch (any e) {
        rethrow
    }
}


describe = tinyTest.describe
it = tinyTest.it
expect = tinyTest.expect
fail = tinyTest.fail
</cfscript>

120 lines now.

What functionality has all this added? Well here's the thing with BDD-style tests. I have documentation and proof that it does what it says it does:

Tests of TinyTestFramework
Tests of it
It prefixes its message with it: OK
Tests of expect
It exists: OK
It returns a struct with keys for matcher callbacks: OK
Tests of fail
It fails a test: OK
Test of test result visualisations
It specifies that a pass should have positive emphasis: OK
It specifies that a fail should have negative emphasis: OK
It specifies that an error should have more emphasis than a fail: OK
Tests of matchers
Tests of toBe
It passes if the actual and expected values are equal: OK
It fails if the actual and expected values are not equal: OK
It expects java.lang.String to work with toBe: OK
It expects java.lang.Double to work with toBe: OK
It expects java.lang.Double to work with toBe: OK
It expects java.lang.Boolean to work with toBe: OK
It expects lucee.runtime.type.ArrayImpl to work with toBe: OK
It expects lucee.runtime.type.StructImpl to work with toBe: OK
It expects lucee.runtime.type.QueryImpl to work with toBe: OK
Tests of notToBe
It passes if the actual and expected values are not equal: OK
It fails if the actual and expected values are equal: OK
It expects java.lang.String to work with notToBe: OK
It expects java.lang.Double to work with notToBe: OK
It expects java.lang.Double to work with notToBe: OK
It expects java.lang.Boolean to work with notToBe: OK
It expects lucee.runtime.type.ArrayImpl to work with notToBe: OK
It expects lucee.runtime.type.StructImpl to work with notToBe: OK
It expects lucee.runtime.type.QueryImpl to work with notToBe: OK
Tests of toBeTrue
It passes if the value is true: OK
It fails if the value is false: OK
Tests of notToBeTrue
It passes if the value is not true: OK
It fails if the value is true: OK
Tests of toBeFalse
It passes if the value is false: OK
It fails if the value is true: OK
Tests of notToBeFalse
It passes if the value is not false: OK
It fails if the value is false: OK
Tests of toThrow
It expects an exception to be thrown from its callback argument: OK
It fails if an exception is not thrown from its callback argument: OK
Tests of notToThrow
It passes if the callback does not throw an exception: OK
It fails if the callback does throw an exception: OK
Tests of toBeNull
It passes if the value is null: OK
It fails if the value is not null: OK
Tests of notToBeNull
It passes if the value is not null: OK
It fails if the value is null: OK
Tests of toInclude
It passes if the haystack contains the needle: OK
It passes if the haystack and needle exactly match: OK
It ignores case: OK
It fails if the haystack does not contain the needle: OK
Tests of notToInclude
It passes if a haystack does not contains the needle: OK
It fails if the haystack contains the needle: OK

All nicely indented and emphasised and shit.

So what have I added for this new version?

  • Tidied up the output to make it easier to read
  • Added these matchers:
    • toBeTrue
    • toBeFalse
    • toBeNull
    • toThrow
    • toInclude
  • And not versions of each of those: notToBe, notToThrow, etc
  • Tests for everything: adamcameron/testTinyTestFramework.cfm

And it's tested using itself, obviously. Interestingly / predictably, there >300 lines of test code there. The ratio is 1:2.5 code:tests

What am I gonna do next? I want to improve that toInclude matcher to work on more than just strings. I also want to have a toBeInstanceOf matcher. Also at some point I better do something with checking structs and arrays and that sorta jazz. I've not needed to actually do that stuff yet, so have not bothered to implement them. But I intend to.

Oh… and you can use this yerself in trycf.com via this URL: https://trycf.com/gist/c631c1f47c8addb2d9aa4d7dacad114f/lucee5?setupCodeGistId=816ce84fd991c2682df612dbaf1cad11&theme=monokai.

Righto.

--
Adam

Saturday, 30 October 2021

TDD: writing a micro testing framework, using the framework to test itself as I build it

G'day:

Being back in CFML land, I spend a lot of time using trycf.com to write sample code. Sometimes I just want to see how ColdFusion and Lucee behave differently in a given situation. Sometimes I'm "helping" someone on the CFML Slack channel, and want to give them a runnable example of what I think they might want to be doing. trycf.com is cool, but I usually want to write my example code as a series of tests demonstrating variants of its behaviour. I also generally want to TDD even sample code that I write, so I know I'm staying on-point and the code works without any surprises. trycf.com is a bit basic, one can only really have one CFML script and run that. One cannot import other libraries like TestBox so I can write my code the way I want.

Quite often I write a little stub tester to help me, and I find myself doing this over and over again, and I never think to save these stubs across examples. Plus having to include the stubs along with the sample code I'm writing clutters things a bit, and also means my example code doesn't really stick to the Single Responsibility Principle.

I found out from Abram - owner of trycf.com - that one can specify a setupCodeGistId in the trycf.com URL, and it will load a CFML script from that and pre-include that before the scratch-pad code. This is bloody handy, and armed with this knowledge I decided I was gonna knock together a minimal single-file testing framework which I could include transparently in with a scratch-pad file, so that scratch-pad file could focus on the sample code I'm writing, and the testing of the same.

A slightly hare-brained idea I have had when doing this is to TDD the whole exercise… and using the test framework to test itself as it evolves. Obviously to start with the test will need to be separate pieces of code before the framework is usable, but beyond a point it should work enough for me to be able to use it to test the latter stages of its development. Well: we'll see, anyhow.

Another challenge I am setting for myself is that I'm gonna mimic the "syntax" of TestBox, so that the tests I write in a scratch-pad should be able to be lifted-out as-is and dumped into a TestBox test spec CFC. Obviously I'm not going to reimplement all of TestBox, just a subset of stuff to be able to do simple tests. I think I will need to implement the following functions:

  • void run() - well, like in TestBox all my tests will be implemented in a function called run, and then one runs that to execute the tests. This is just convention rather than needing any development.
  • void describe(required string label, required function testGroup) - this will output its label and call its testGroup.
  • void it(required string label, required function implementation) - this will output its label and call its implementation. I'm pretty sure the implementation of describe and it will be identical. I mean like I will use two references to the same function to implement this.
  • struct expect(required any actual) - this will take the actual value being tested, and return a struct containing a matcher to that actual value.
  • boolean toBe(required any expected) - this will take the value that the actual value passed to expect should be. This will be as a key in the struct returned by expect (this will emulate the function-chaining TestBox uses with expect(x).toBe(y).

If I create that minimalist implementation, then I will be able to write this much of a test suite in a trycf.com scratch pad:

function myFunctionToTest(x) {
    // etc
}

function run() {
    describe("Tests for myFunctionToTest" ,() => {
        it("tests some variant", () => {
            expect(myFunctionToTest("variant a")).toBe("something")
        })

        it("tests some other variant", () => {
            expect(myFunctionToTest("variant b")).toBe("something else")
        })
    })
}

run()

And everything in that run function will be compatible with TestBox.

I am going to show every single iteration of the process here, to demonstrate TDD in action. This means this article will be long, and it will be code-heavy. And it will have a lot of repetition. I'll try to keep my verbiage minimal, so if I think the iteration of the code speaks for itself, I possibly won't have anything to add.

Let's get on with it.


It runs the tests via a function "run"

<cfscript>
// tests    
try {
    run()
    writeOutput("OK")
} catch (any e) {
    writeOutput("run function not found")
}
</cfscript>

<cfscript>
// implementation    
    
</cfscript>

I am going to do this entire implementation in a trycf.com scratch-pad file. As per above, I have two code blocks: tests and implementation. For each step I will show you the test (or updates to existing tests), and then I will show you the implementation. We can take it as a given that the test will fail in the way I expect (which will be obvious from the code), unless I state otherwise. As a convention a test will output "OK" if it passed, or "Failure: " and some error explanation if not. Later these will be baked into the framework code, but for now, it's hand-cranked. This shows that you don't need any framework to design your code via TDD: it's a practice, it's not a piece of software.

When I run the code above, I get "Failure: run function not found". This is obviously because the run function doesn't even exist yet. Let's address that.

// implementation    
void function run(){
}

Results: OK

We have completed one iteration of red/green. There is nothing to refactor yet. Onto the next iteration.


It has a function describe

We already have some of our framework operational. We can put tests into that run function, and they'll be run: that's about all that function needs to do. Hey I didn't say this framework was gonna be complicated. In fact the aim is for it to be the exact opposite of complicated.

// tests
// ...

void function run(){
    try {
        describe()
        writeOutput("OK")
    } catch (any e) {
        writeOutput("Failure: describe function not found")
    }
}
// implementation
void function describe(){
}

The tests already helped me here actually. In my initial effort at implementing describe, I misspelt it as "desribe". Took me a few seconds to spot why the test failed. I presume, like me, you have found a function with a spelling mistake in it that has escaped into production. I was saved that here, by having to manually type the name of the function into the test before I did the first implementation.


describe takes a string parameter label which is displayed on a line by itself as a label for the test grouping

// tests
// ...
savecontent variable="testOutput" {
    describe("TEST_DESCRIPTION")
};
if (testOutput == "TEST_DESCRIPTION<br>") {
    writeOutput("OK<br>")
    return
}
writeOutput("Failure: label not output<br>")
// implementation
void function describe(required string label){
    writeOutput("#label#<br>")
}

This implementation passes its own test (good), but it makes the previous test break:

try {
    describe()
    writeOutput("OK")
} catch (any e) {
    writeOutput("Failure: describe function not found")
}

We require describe to take an argument now, so we need to update that test slightly:

try {
    describe("NOT_TESTED")
    writeOutput("OK")
} catch (any e) {
    writeOutput("Failure: describe function not found")
}

I make a point of being very clear when arguments etc I need to use are not part of the test.

It's also worth noting that the test output is a bit of a mess at the moment:

OKNOT_TESTED
OKOK

For the sake of cosmetics, I've gone through and put <br> tags on all the test messages I'm outputting, and I've also slapped a cfsilent around that first describe test, as its output is just clutter. The full implementation is currently:

// tests    
try {
    cfsilent() {
        run()
    }
    writeOutput("OK<br>")
} catch (any e) {
    writeOutput("Failure: run function not found<br>")
}

void function run(){
    try {
        cfsilent(){describe("NOT_TESTED")}
        writeOutput("OK<br>")
    } catch (any e) {
        writeOutput("Failure: describe function not found<br>")
    }

    savecontent variable="testOutput" {
        describe("TEST_DESCRIPTION")
    };
    if (testOutput == "TEST_DESCRIPTION<br>") {
        writeOutput("OK<br>")
    }else{
    	writeOutput("Failure: label not output<br>")
    }
}

run()
</cfscript>

<cfscript>
// implementation
void function describe(required string label){
    writeOutput("#label#<br>")
}

And now the output is tidier:

OK
OK
OK

I actually did a double-take here, wondering why the TEST_DESCRIPTION message was not displaying for that second test: the one where I'm actually testing that that message displays. Of course it's because I've got the savecontent around it, so I'm capturing the output, not letting it output. Duh.


describe takes a callback parameter testGroup which is is executed after the description is displayed

savecontent variable="testOutput" {
    describe("TEST_DESCRIPTION", () => {
        writeOutput("DESCRIBE_GROUP_OUTPUT")
    })
};
if (testOutput == "TEST_DESCRIPTION<br>DESCRIBE_GROUP_OUTPUT") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: testGroup not executed<br>")
}
void function describe(required string label, required function testGroup){
    writeOutput("#label#<br>")
    testGroup()
}

This implementation change broke earlier tests that called describe, because they were not passing a testGroup argument. I've updated those to just slung an empty callback into those calls, eg:

describe("TEST_DESCRIPTION", () => {})

That's all I really need from describe, really. But I'll just do one last test to confirm that it can be nested OK (there's no reason why it couldn't be, but I'm gonna check anyways.


describe calls can be nested

savecontent variable="testOutput" {
    describe("OUTER_TEST_DESCRIPTION", () => {
        describe("INNER_TEST_DESCRIPTION", () => {
            writeOutput("DESCRIBE_GROUP_OUTPUT")
        })
    })
};
if (testOutput == "OUTER_TEST_DESCRIPTION<br>INNER_TEST_DESCRIPTION<br>DESCRIBE_GROUP_OUTPUT") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: describe nested did not work<br>")
}

And this passes without any adjustment to the implementation, as I expected. Note that it's OK to write a test that just passes, if there's not then a need to make an implementation change to make it pass. One only needs a failing test before one makes an implementation change. Remember the tests are to test that the implementation change does what it's supposed to. This test here is just a belt and braces thing, and to be declarative about some functionality the system has.


The it function behaves the same way as the describe function

it will end up having more required functionality than describe needs, but there's no reason for them to not just be aliases of each other for the purposes of this micro-test-framework. As long as the describe alias still passes all its tests, it doesn't matter what extra functionality I put into it to accommodate the requirements of it.

savecontent variable="testOutput" {
    describe("TEST_DESCRIPTION", () => {
        it("TEST_CASE_DESCRIPTION", () => {
            writeOutput("TEST_CASE_RESULT")
        })
    })
};
if (testOutput == "TEST_DESCRIPTION<br>TEST_CASE_DESCRIPTION<br>TEST_CASE_RESULT") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not work<br>")
}
it = describe

it will not error-out if an exception occurs in its callback, instead reporting an error result, with the exception's message

Tests are intrinsically the sort of thing that might break, so we can't have the test run stopping just cos an exception occurs.

savecontent variable="testOutput" {
    describe("NOT_TESTED", () => {
        it("tests an exception", () => {
            throw "EXCEPTION_MESSAGE";
        })
    })
};
if (testOutput CONTAINS "tests an exception<br>Error: EXCEPTION_MESSAGE<br>") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not correctly report the test error<br>")
}
void function describe(required string label, required function testGroup) {
    try {
        writeOutput("#label#<br>")
        testGroup()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

At this point I decided I didn't like how I was just using describe to implement it, so I refactored:

void function runLabelledCallback(required string label, required function callback) {
    try {
        writeOutput("#label#<br>")
        callback()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

void function describe(required string label, required function testGroup) {
    runLabelledCallback(label, testGroup)
}

void function it(required string label, required function implementation) {
    runLabelledCallback(label, implementation)
}

Because everything is test-covered, I am completely safe to just make that change. Now I have the correct function signature on describe and it, and an appropriately "general" function to do the actual execution. And the tests all still pass, so that's cool. Reminder: refactoring doesn't need to start with a failing test. Intrinsically it's an activity that mustn't impact the behaviour of the code being refactored, otherwise it's not a refactor. Also note that one does not ever alter implementation and refactor at the same time. Follow red / green / refactor as separate steps.


it outputs "OK" if the test ran correctly

// <samp>it</samp> outputs OK if the test ran correctly
savecontent variable="testOutput" {
    describe("NOT_TESTED", () => {
        it("outputs OK if the test ran correctly", () => {
            // some test here... this actually starts to demonstrate an issue with the implementation, but we'll get to that
        })
    })
};
if (testOutput CONTAINS "outputs OK if the test ran correctly<br>OK<br>") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not correctly report the test error<br>")
}

The implementation of this demonstrated that describe and it can't share an implementation. I don't want describe calls outputting "OK" when their callback runs OK, and this was what started happening when I did my first pass of the implementation for this:

void function runLabelledCallback(required string label, required function callback) {
    try {
        writeOutput("#label#<br>")
        callback()
        writeOutput("OK<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

Only it is a test, and only it is supposed to say "OK". This is an example of premature refactoring, and probably going overboard with DRY. The describe function wasn't complex, so we were gaining nothing by de-duping it from it, especially when it's implementation will have more complexity still to come.

I backed out my implementation and my failing test for a moment, and did another refactor to separate-out the two functions completely. I know I'm good when all my tests still pass.

void function describe(required string label, required function testGroup) {
    writeOutput("#label#<br>")
    testGroup()
}

void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#<br>")
        implementation()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

I also needed to update the baseline it test to expect the "OK<br>". After that everything is green again, and I can bring my new test back (failure as expected), and implementation (all green again/still):

void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#<br>")
        implementation()
        writeOutput("OK<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

describe will not error-out if an exception occurs in its callback, instead reporting an error result, with the exception's message

Back to describe again. It too needs to not error-out if there's an issue with its callback, so we're going to put in some handling for that again too. This test is largely copy and paste from the equivalent it test:

savecontent variable="testOutput" {
    describe("TEST_DESCRIPTION", () => {
        throw "EXCEPTION_MESSAGE";
    })
};
if (testOutput CONTAINS "TEST_DESCRIPTION<br>Error: EXCEPTION_MESSAGE<br>") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not correctly report the test error<br>")
}
void function describe(required string label, required function testGroup) {
    try {
        writeOutput("#label#<br>")
        testGroup()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

it outputs "Failed" if the test failed

This took some thought. How do I know a test "failed"? In TestBox when something like expect(true).toBeFalse() runs, it exits from the test immediately, and does not run any further expectations the test might have. Clearly it's throwing an exception from toBeFalse if the actual value (the one passed to expect) isn't false. So this is what I need to do. I have not written any assertions or expectations yet, so for now I'll just test with an actual exception. It can't be any old exception, because the test could break for any other reason too, so I need to differentiate between a test fail exception (the test has failed), and any other sort of exception (the test errored). I'll use a TestFailedException!

savecontent variable="testOutput" {
    describe("NOT_TESTED", () => {
        it("outputs failed if the test failed", () => {
            throw(type="TestFailedException");
        })
    })
};
if (testOutput.reFind("Failed<br>$")) {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not correctly report the test failure<br>")
}
void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#<br>")
        implementation()
        writeOutput("OK<br>")
    } catch (TestFailedException e) {
        writeOutput("Failed<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

This is probably enough now to a) start using the framework for its own tests; b) start working on expect and toBe.


has a function expect

it("has a function expect", () => {
    expect()
})

Check. It. Out. I'm actually able to use the framework now. I run this and I get:

has a function expect
Error: Variable EXPECT is undefined.

And when I do the implementation:

function expect() {
}
has a function expect
OK

I think once I'm tidying up, I might look to move the test result to be on the same line as the label. We'll see. For now though: functionality.


expect returns a struct with a key toBe which is a function

it("expect returns a struct with a key toBe which is a function", () => {
    var result = expect()
    if (isNull(result) || isNull(result.toBe) || !isCustomFunction(local.result.toBe)) {
        throw(type="TestFailedException")
    }
})

I'd like to be able to just use !isCustomFunction(local?.result?.toBe) in that if statement there, but Lucee has a bug that prevents ?. to be used in a function expression (I am not making this up, go look at LDEV-3020). Anyway, the implementation for this for now is easy:

function expect() {
    return {toBe = () => {}}
}

toBe returns true if the actual and expected values are equal

it("toBe returns true if the actual and expected values are equal", () => {
    var actual = "TEST_VALUE"
    var expected = "TEST_VALUE"

    result = expect(actual).toBe(expected)
    if (isNull(result) || !result) {
        throw(type="TestFailedException")
    }
})

The implementation for this bit is deceptively easy:

function expect(required any actual) {
    return {toBe = (expected) => {
        return actual.equals(expected)
    }}
}

toBe throws a TestFailedException if the actual and expected values are not equal

it("toBe throws a TestFailedException if the actual and expected values are not equal", () => {
    var actual = "ACTUAL_VALUE"
    var expected = "EXPECTED_VALUE"

    try {
        expect(actual).toBe(expected)
    } catch (TestFailedException e) {
        return
    }
    throw(type="TestFailedException")
})
function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TestFailedException")
    }}
}

And with that… I have a very basic testing framework. Obviously it could be improved to have more expectations (toBeTrue, toBeFalse, toBe{Type}), and could have some nice messages on the toBe function so a failure is more clear as to what went on, but for a minimum viable project, this is fine. I'm going to do a couple more tests / tweaks though.


toBe works with a variety of data types

var types = ["string", 0, 0.0, true, ["array"], {struct="struct"}, queryNew(""), xmlNew()]
types.each((type) => {
    it("works with #type.getClass().getName()#", (type) => {
        expect(type).toBe(type)
    })
})

The results here differ between Lucee:

works with java.lang.String
OK
works with java.lang.Double
OK
works with java.lang.Double
OK
works with java.lang.Boolean
OK
works with lucee.runtime.type.ArrayImpl
OK
works with lucee.runtime.type.StructImpl
OK
works with lucee.runtime.type.QueryImpl
OK
works with lucee.runtime.text.xml.struct.XMLDocumentStruct
Failed

And ColdFusion:

works with java.lang.String
OK
works with java.lang.Integer
OK
works with coldfusion.runtime.CFDouble
Failed
works with coldfusion.runtime.CFBoolean
OK
works with coldfusion.runtime.Array
OK
works with coldfusion.runtime.Struct
OK
works with coldfusion.sql.QueryTable
OK
works with org.apache.xerces.dom.DocumentImpl
Failed

But the thing is the tests actually work on both platforms. If you compare the objects outside the context of the test framework, the results are the same. Apparently in ColdFusion 0.0 does not equal itself. I will be taking this up with Adobe, I think.

It's good to know that it works OK for structs, arrays and queries though.


The it function puts the test result on the same line as the test label, separated by a colon

As I mentioned above, I'd gonna take the <br> out from between the test message and the result, instead just colon-separating them:

// The <samp>it</samp> function puts the test result on the same line as the test label, separated by a colon
savecontent variable="testOutput" {
    describe("TEST_DESCRIPTION", () => {
        it("TEST_CASE_DESCRIPTION", () => {
            writeOutput("TEST_CASE_RESULT")
        })
    })
};
if (testOutput == "TEST_DESCRIPTION<br>TEST_CASE_DESCRIPTION: TEST_CASE_RESULTOK<br>") {
    writeOutput("OK<br>")
}else{
    writeOutput("Failure: the it function did not work<br>")
}
void function it(required string label, required function implementation) {
    try {
    	writeOutput("#label#<br>")
        writeOutput("#label#: ")
        implementation()
        writeOutput("OK<br>")
    } catch (TestFailedException e) {
        writeOutput("Failed<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

That's just a change to that line before calling implementation()


Putting it to use

I can now save the implementation part of this to a gist, save another gist with my actual tests in it, and load that into trycf.com with the setupCodeGistId param pointing to my test framework Gist: https://trycf.com/gist/c631c1f47c8addb2d9aa4d7dacad114f/lucee5?setupCodeGistId=816ce84fd991c2682df612dbaf1cad11&theme=monokai. And… (sigh of relief, cos I'd not tried this until now) it all works.

Update 2022-05-06

That gist points to a newer version of this work. I made a breaking change to how it works today, but wanted the link here to still work.


Outro

This was a long one, but a lot of it was really simple code snippets, so hopefully it doesn't overflow the brain to read it. If you'd like me to clarify anything or find any bugs or I've messed something up or whatever, let me know. Also note that whilst it'll take you a while to read, and it took me bloody ages to write, if I was just doing the actual TDD exercise it's only about an hour's effort. The red/green/refactor cycle is very short.

Oh! Speaking of implementations. I never showed the final product. It's just this:

void function describe(required string label, required function testGroup) {
    try {
        writeOutput("#label#<br>")
        testGroup()
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

void function it(required string label, required function implementation) {
    try {
        writeOutput("#label#: ")
        implementation()
        writeOutput("OK<br>")
    } catch (TestFailedException e) {
        writeOutput("Failed<br>")
    } catch (any e) {
        writeOutput("Error: #e.message#<br>")
    }
}

struct function expect(required any actual) {
    return {toBe = (expected) => {
        if (actual.equals(expected)) {
            return true
        }
        throw(type="TestFailedException")
    }}
}

That's a working testing framework. I quite like this :-)

All the code for the entire exercise - including all the test code - can be checked-out on trycf.com.

Righto.

--
Adam